ai-engineering-init 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/code-reviewer.md +139 -0
- package/.claude/agents/project-manager.md +159 -0
- package/.claude/audio/completed.wav +0 -0
- package/.claude/commands/add-todo.md +255 -0
- package/.claude/commands/check.md +210 -0
- package/.claude/commands/crud.md +454 -0
- package/.claude/commands/dev.md +503 -0
- package/.claude/commands/init-docs.md +681 -0
- package/.claude/commands/next.md +251 -0
- package/.claude/commands/progress.md +242 -0
- package/.claude/commands/start.md +199 -0
- package/.claude/commands/sync.md +307 -0
- package/.claude/commands/update-status.md +428 -0
- package/.claude/docs/Mixin/344/275/277/347/224/250/346/214/207/345/215/227.md +299 -0
- package/.claude/docs/README.md +167 -0
- package/.claude/docs//345/211/215/347/253/257/345/274/200/345/217/221/346/214/207/345/215/227.md +599 -0
- package/.claude/docs//345/220/216/347/253/257/345/274/200/345/217/221/346/214/207/345/215/227.md +726 -0
- package/.claude/docs//345/267/245/344/275/234/346/265/201/345/274/200/345/217/221/346/214/207/345/215/227.md +714 -0
- package/.claude/docs//345/267/245/345/205/267/347/261/273/344/275/277/347/224/250/346/214/207/345/215/227.md +463 -0
- package/.claude/docs//346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/350/247/204/350/214/203.md +390 -0
- package/.claude/docs//346/226/260/345/212/237/350/203/275/345/274/200/345/217/221/346/265/201/347/250/213/350/247/204/350/214/203.md +688 -0
- package/.claude/docs//346/226/260/351/241/271/347/233/256/345/274/200/345/217/221/346/265/201/347/250/213.md +365 -0
- package/.claude/docs//346/241/206/346/236/266/350/257/264/346/230/216.md +393 -0
- package/.claude/docs//350/267/257/347/224/261/351/205/215/347/275/256/346/214/207/345/215/227.md +246 -0
- package/.claude/framework-config.json +73 -0
- package/.claude/hooks/pre-tool-use.js +117 -0
- package/.claude/hooks/skill-forced-eval.js +167 -0
- package/.claude/hooks/stop.js +58 -0
- package/.claude/settings.json +41 -0
- package/.claude/skills/add-skill/SKILL.md +352 -0
- package/.claude/skills/api-development/SKILL.md +560 -0
- package/.claude/skills/architecture-design/SKILL.md +756 -0
- package/.claude/skills/backend-annotations/SKILL.md +674 -0
- package/.claude/skills/banana-image/CHANGELOG.md +37 -0
- package/.claude/skills/banana-image/README.md +146 -0
- package/.claude/skills/banana-image/SKILL.md +164 -0
- package/.claude/skills/banana-image/assets/logo.png +0 -0
- package/.claude/skills/banana-image/references/advanced-usage.md +189 -0
- package/.claude/skills/banana-image/scripts/apply_template.py +125 -0
- package/.claude/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/.claude/skills/banana-image/scripts/batch_prep.py +82 -0
- package/.claude/skills/banana-image/scripts/package-lock.json +1437 -0
- package/.claude/skills/banana-image/scripts/package.json +18 -0
- package/.claude/skills/banana-image/scripts/requirements.txt +10 -0
- package/.claude/skills/banana-image/templates/poster.json +22 -0
- package/.claude/skills/banana-image/templates/product.json +17 -0
- package/.claude/skills/banana-image/templates/social.json +22 -0
- package/.claude/skills/banana-image/templates/thumbnail.json +17 -0
- package/.claude/skills/brainstorm/SKILL.md +648 -0
- package/.claude/skills/bug-detective/SKILL.md +1206 -0
- package/.claude/skills/code-patterns/SKILL.md +590 -0
- package/.claude/skills/collaborating-with-codex/SKILL.md +174 -0
- package/.claude/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/.claude/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/.claude/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/.claude/skills/crud-development/SKILL.md +649 -0
- package/.claude/skills/data-permission/SKILL.md +599 -0
- package/.claude/skills/database-ops/SKILL.md +407 -0
- package/.claude/skills/error-handler/SKILL.md +371 -0
- package/.claude/skills/file-oss-management/SKILL.md +863 -0
- package/.claude/skills/git-workflow/SKILL.md +375 -0
- package/.claude/skills/json-serialization/SKILL.md +357 -0
- package/.claude/skills/leniu-api-development/SKILL.md +803 -0
- package/.claude/skills/leniu-architecture-design/SKILL.md +598 -0
- package/.claude/skills/leniu-backend-annotations/SKILL.md +664 -0
- package/.claude/skills/leniu-code-patterns/SKILL.md +365 -0
- package/.claude/skills/leniu-crud-development/SKILL.md +1110 -0
- package/.claude/skills/leniu-data-permission/SKILL.md +256 -0
- package/.claude/skills/leniu-database-ops/SKILL.md +426 -0
- package/.claude/skills/leniu-error-handler/SKILL.md +462 -0
- package/.claude/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.claude/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.claude/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/.claude/skills/leniu-java-entity/SKILL.md +751 -0
- package/.claude/skills/leniu-java-export/SKILL.md +560 -0
- package/.claude/skills/leniu-java-logging/SKILL.md +832 -0
- package/.claude/skills/leniu-java-mq/SKILL.md +338 -0
- package/.claude/skills/leniu-java-mybatis/SKILL.md +640 -0
- package/.claude/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/.claude/skills/leniu-java-task/SKILL.md +367 -0
- package/.claude/skills/leniu-java-total-line/SKILL.md +195 -0
- package/.claude/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/.claude/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/.claude/skills/leniu-mealtime/SKILL.md +215 -0
- package/.claude/skills/leniu-redis-cache/SKILL.md +316 -0
- package/.claude/skills/leniu-security-guard/SKILL.md +520 -0
- package/.claude/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
- package/.claude/skills/openspec-explore/SKILL.md +290 -0
- package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
- package/.claude/skills/openspec-new-change/SKILL.md +74 -0
- package/.claude/skills/openspec-onboard/SKILL.md +529 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
- package/.claude/skills/performance-doctor/SKILL.md +627 -0
- package/.claude/skills/project-navigator/SKILL.md +305 -0
- package/.claude/skills/redis-cache/SKILL.md +839 -0
- package/.claude/skills/scheduled-jobs/SKILL.md +633 -0
- package/.claude/skills/security-guard/SKILL.md +748 -0
- package/.claude/skills/sms-mail/SKILL.md +766 -0
- package/.claude/skills/social-login/SKILL.md +668 -0
- package/.claude/skills/store-pc/SKILL.md +366 -0
- package/.claude/skills/task-tracker/SKILL.md +307 -0
- package/.claude/skills/tech-decision/SKILL.md +393 -0
- package/.claude/skills/tenant-management/SKILL.md +603 -0
- package/.claude/skills/test-development/SKILL.md +755 -0
- package/.claude/skills/ui-pc/SKILL.md +438 -0
- package/.claude/skills/utils-toolkit/SKILL.md +615 -0
- package/.claude/skills/websocket-sse/SKILL.md +716 -0
- package/.claude/skills/workflow-engine/SKILL.md +676 -0
- package/.claude/templates//345/276/205/345/212/236/346/270/205/345/215/225/346/250/241/346/235/277.md +56 -0
- package/.claude/templates//351/234/200/346/261/202/346/226/207/346/241/243/346/250/241/346/235/277.md +85 -0
- package/.claude/templates//351/241/271/347/233/256/347/212/266/346/200/201/346/250/241/346/235/277.md +43 -0
- package/.codex/skills/add-skill/SKILL.md +352 -0
- package/.codex/skills/add-todo/SKILL.md +269 -0
- package/.codex/skills/api-development/SKILL.md +693 -0
- package/.codex/skills/architecture-design/SKILL.md +628 -0
- package/.codex/skills/backend-annotations/SKILL.md +664 -0
- package/.codex/skills/banana-image/CHANGELOG.md +37 -0
- package/.codex/skills/banana-image/README.md +146 -0
- package/.codex/skills/banana-image/SKILL.md +164 -0
- package/.codex/skills/banana-image/assets/logo.png +0 -0
- package/.codex/skills/banana-image/references/advanced-usage.md +189 -0
- package/.codex/skills/banana-image/scripts/apply_template.py +125 -0
- package/.codex/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/.codex/skills/banana-image/scripts/batch_prep.py +82 -0
- package/.codex/skills/banana-image/scripts/package-lock.json +1437 -0
- package/.codex/skills/banana-image/scripts/package.json +18 -0
- package/.codex/skills/banana-image/scripts/requirements.txt +10 -0
- package/.codex/skills/banana-image/templates/poster.json +22 -0
- package/.codex/skills/banana-image/templates/product.json +17 -0
- package/.codex/skills/banana-image/templates/social.json +22 -0
- package/.codex/skills/banana-image/templates/thumbnail.json +17 -0
- package/.codex/skills/brainstorm/SKILL.md +648 -0
- package/.codex/skills/bug-detective/SKILL.md +1206 -0
- package/.codex/skills/check/SKILL.md +367 -0
- package/.codex/skills/code-patterns/SKILL.md +442 -0
- package/.codex/skills/collaborating-with-codex/SKILL.md +174 -0
- package/.codex/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/.codex/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/.codex/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/.codex/skills/crud/SKILL.md +265 -0
- package/.codex/skills/crud-development/SKILL.md +637 -0
- package/.codex/skills/data-permission/SKILL.md +591 -0
- package/.codex/skills/database-ops/SKILL.md +553 -0
- package/.codex/skills/dev/SKILL.md +187 -0
- package/.codex/skills/error-handler/SKILL.md +361 -0
- package/.codex/skills/file-oss-management/SKILL.md +863 -0
- package/.codex/skills/git-workflow/SKILL.md +375 -0
- package/.codex/skills/init-docs/SKILL.md +194 -0
- package/.codex/skills/json-serialization/SKILL.md +357 -0
- package/.codex/skills/leniu-api-development/SKILL.md +803 -0
- package/.codex/skills/leniu-architecture-design/SKILL.md +594 -0
- package/.codex/skills/leniu-backend-annotations/SKILL.md +662 -0
- package/.codex/skills/leniu-code-patterns/SKILL.md +365 -0
- package/.codex/skills/leniu-crud-development/SKILL.md +1110 -0
- package/.codex/skills/leniu-data-permission/SKILL.md +256 -0
- package/.codex/skills/leniu-database-ops/SKILL.md +426 -0
- package/.codex/skills/leniu-error-handler/SKILL.md +462 -0
- package/.codex/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.codex/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.codex/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/.codex/skills/leniu-java-entity/SKILL.md +751 -0
- package/.codex/skills/leniu-java-export/SKILL.md +560 -0
- package/.codex/skills/leniu-java-logging/SKILL.md +832 -0
- package/.codex/skills/leniu-java-mq/SKILL.md +338 -0
- package/.codex/skills/leniu-java-mybatis/SKILL.md +640 -0
- package/.codex/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/.codex/skills/leniu-java-task/SKILL.md +367 -0
- package/.codex/skills/leniu-java-total-line/SKILL.md +195 -0
- package/.codex/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/.codex/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/.codex/skills/leniu-mealtime/SKILL.md +215 -0
- package/.codex/skills/leniu-redis-cache/SKILL.md +316 -0
- package/.codex/skills/leniu-security-guard/SKILL.md +520 -0
- package/.codex/skills/leniu-utils-toolkit/SKILL.md +378 -0
- package/.codex/skills/next/SKILL.md +137 -0
- package/.codex/skills/openspec-apply-change/SKILL.md +156 -0
- package/.codex/skills/openspec-archive-change/SKILL.md +114 -0
- package/.codex/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.codex/skills/openspec-continue-change/SKILL.md +118 -0
- package/.codex/skills/openspec-explore/SKILL.md +290 -0
- package/.codex/skills/openspec-ff-change/SKILL.md +101 -0
- package/.codex/skills/openspec-new-change/SKILL.md +74 -0
- package/.codex/skills/openspec-onboard/SKILL.md +529 -0
- package/.codex/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.codex/skills/openspec-verify-change/SKILL.md +168 -0
- package/.codex/skills/performance-doctor/SKILL.md +627 -0
- package/.codex/skills/progress/SKILL.md +193 -0
- package/.codex/skills/project-navigator/SKILL.md +286 -0
- package/.codex/skills/redis-cache/SKILL.md +829 -0
- package/.codex/skills/scheduled-jobs/SKILL.md +633 -0
- package/.codex/skills/security-guard/SKILL.md +739 -0
- package/.codex/skills/sms-mail/SKILL.md +766 -0
- package/.codex/skills/social-login/SKILL.md +668 -0
- package/.codex/skills/start/SKILL.md +154 -0
- package/.codex/skills/store-pc/SKILL.md +491 -0
- package/.codex/skills/sync/SKILL.md +149 -0
- package/.codex/skills/task-tracker/SKILL.md +307 -0
- package/.codex/skills/tech-decision/SKILL.md +393 -0
- package/.codex/skills/tenant-management/SKILL.md +603 -0
- package/.codex/skills/test-development/SKILL.md +755 -0
- package/.codex/skills/ui-pc/SKILL.md +475 -0
- package/.codex/skills/update-status/SKILL.md +159 -0
- package/.codex/skills/utils-toolkit/SKILL.md +593 -0
- package/.codex/skills/websocket-sse/SKILL.md +716 -0
- package/.codex/skills/workflow-engine/SKILL.md +676 -0
- package/.cursor/agents/code-reviewer.md +139 -0
- package/.cursor/agents/project-manager.md +159 -0
- package/.cursor/commands/opsx-apply.md +152 -0
- package/.cursor/commands/opsx-archive.md +157 -0
- package/.cursor/commands/opsx-bulk-archive.md +242 -0
- package/.cursor/commands/opsx-continue.md +114 -0
- package/.cursor/commands/opsx-explore.md +174 -0
- package/.cursor/commands/opsx-ff.md +94 -0
- package/.cursor/commands/opsx-new.md +69 -0
- package/.cursor/commands/opsx-onboard.md +525 -0
- package/.cursor/commands/opsx-sync.md +134 -0
- package/.cursor/commands/opsx-verify.md +164 -0
- package/.cursor/mcp.json +22 -0
- package/.cursor/skills/add-skill/SKILL.md +352 -0
- package/.cursor/skills/api-development/SKILL.md +560 -0
- package/.cursor/skills/architecture-design/SKILL.md +756 -0
- package/.cursor/skills/backend-annotations/SKILL.md +674 -0
- package/.cursor/skills/banana-image/CHANGELOG.md +37 -0
- package/.cursor/skills/banana-image/README.md +146 -0
- package/.cursor/skills/banana-image/SKILL.md +164 -0
- package/.cursor/skills/banana-image/assets/logo.png +0 -0
- package/.cursor/skills/banana-image/references/advanced-usage.md +189 -0
- package/.cursor/skills/banana-image/scripts/apply_template.py +125 -0
- package/.cursor/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/.cursor/skills/banana-image/scripts/batch_prep.py +82 -0
- package/.cursor/skills/banana-image/scripts/package-lock.json +1437 -0
- package/.cursor/skills/banana-image/scripts/package.json +18 -0
- package/.cursor/skills/banana-image/scripts/requirements.txt +10 -0
- package/.cursor/skills/banana-image/templates/poster.json +22 -0
- package/.cursor/skills/banana-image/templates/product.json +17 -0
- package/.cursor/skills/banana-image/templates/social.json +22 -0
- package/.cursor/skills/banana-image/templates/thumbnail.json +17 -0
- package/.cursor/skills/brainstorm/SKILL.md +648 -0
- package/.cursor/skills/bug-detective/SKILL.md +1206 -0
- package/.cursor/skills/code-patterns/SKILL.md +590 -0
- package/.cursor/skills/collaborating-with-codex/SKILL.md +174 -0
- package/.cursor/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/.cursor/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/.cursor/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/.cursor/skills/crud-development/SKILL.md +649 -0
- package/.cursor/skills/data-permission/SKILL.md +599 -0
- package/.cursor/skills/database-ops/SKILL.md +407 -0
- package/.cursor/skills/error-handler/SKILL.md +371 -0
- package/.cursor/skills/file-oss-management/SKILL.md +863 -0
- package/.cursor/skills/git-workflow/SKILL.md +375 -0
- package/.cursor/skills/json-serialization/SKILL.md +357 -0
- package/.cursor/skills/leniu-api-development/SKILL.md +803 -0
- package/.cursor/skills/leniu-architecture-design/SKILL.md +598 -0
- package/.cursor/skills/leniu-backend-annotations/SKILL.md +664 -0
- package/.cursor/skills/leniu-code-patterns/SKILL.md +365 -0
- package/.cursor/skills/leniu-crud-development/SKILL.md +1110 -0
- package/.cursor/skills/leniu-data-permission/SKILL.md +256 -0
- package/.cursor/skills/leniu-database-ops/SKILL.md +426 -0
- package/.cursor/skills/leniu-error-handler/SKILL.md +462 -0
- package/.cursor/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.cursor/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.cursor/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/.cursor/skills/leniu-java-entity/SKILL.md +751 -0
- package/.cursor/skills/leniu-java-export/SKILL.md +560 -0
- package/.cursor/skills/leniu-java-logging/SKILL.md +832 -0
- package/.cursor/skills/leniu-java-mq/SKILL.md +338 -0
- package/.cursor/skills/leniu-java-mybatis/SKILL.md +640 -0
- package/.cursor/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/.cursor/skills/leniu-java-task/SKILL.md +367 -0
- package/.cursor/skills/leniu-java-total-line/SKILL.md +195 -0
- package/.cursor/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/.cursor/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/.cursor/skills/leniu-mealtime/SKILL.md +215 -0
- package/.cursor/skills/leniu-redis-cache/SKILL.md +316 -0
- package/.cursor/skills/leniu-security-guard/SKILL.md +520 -0
- package/.cursor/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/.cursor/skills/openspec-apply-change/SKILL.md +156 -0
- package/.cursor/skills/openspec-archive-change/SKILL.md +114 -0
- package/.cursor/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.cursor/skills/openspec-continue-change/SKILL.md +118 -0
- package/.cursor/skills/openspec-explore/SKILL.md +290 -0
- package/.cursor/skills/openspec-ff-change/SKILL.md +101 -0
- package/.cursor/skills/openspec-new-change/SKILL.md +74 -0
- package/.cursor/skills/openspec-onboard/SKILL.md +529 -0
- package/.cursor/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.cursor/skills/openspec-verify-change/SKILL.md +168 -0
- package/.cursor/skills/performance-doctor/SKILL.md +627 -0
- package/.cursor/skills/project-navigator/SKILL.md +305 -0
- package/.cursor/skills/redis-cache/SKILL.md +839 -0
- package/.cursor/skills/scheduled-jobs/SKILL.md +633 -0
- package/.cursor/skills/security-guard/SKILL.md +748 -0
- package/.cursor/skills/sms-mail/SKILL.md +766 -0
- package/.cursor/skills/social-login/SKILL.md +668 -0
- package/.cursor/skills/store-pc/SKILL.md +366 -0
- package/.cursor/skills/task-tracker/SKILL.md +307 -0
- package/.cursor/skills/tech-decision/SKILL.md +393 -0
- package/.cursor/skills/tenant-management/SKILL.md +603 -0
- package/.cursor/skills/test-development/SKILL.md +755 -0
- package/.cursor/skills/ui-pc/SKILL.md +438 -0
- package/.cursor/skills/utils-toolkit/SKILL.md +615 -0
- package/.cursor/skills/websocket-sse/SKILL.md +716 -0
- package/.cursor/skills/workflow-engine/SKILL.md +676 -0
- package/AGENTS.md +669 -0
- package/CLAUDE.md +205 -0
- package/README.md +205 -0
- package/bin/index.js +179 -0
- package/init.sh +178 -0
- package/package.json +27 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: redis-cache
|
|
3
|
+
description: |
|
|
4
|
+
当需要使用Redis缓存、分布式锁、限流等功能时自动使用此Skill。包含RedisUtils工具类、CacheUtils工具类、缓存注解使用规范、分布式锁实现、缓存key命名规范等。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 使用Redis缓存数据
|
|
8
|
+
- 配置Spring Cache缓存注解
|
|
9
|
+
- 实现分布式锁
|
|
10
|
+
- 实现接口限流
|
|
11
|
+
- Redis发布订阅
|
|
12
|
+
- 缓存穿透/雪崩/击穿问题
|
|
13
|
+
- 缓存key设计和命名
|
|
14
|
+
- 缓存过期时间设置
|
|
15
|
+
- 缓存清理和刷新
|
|
16
|
+
|
|
17
|
+
触发词:Redis、缓存、Cache、@Cacheable、@CacheEvict、@CachePut、RedisUtils、CacheUtils、分布式锁、RLock、限流、RateLimiter、发布订阅、缓存穿透、缓存雪崩、缓存击穿、缓存key、缓存过期、缓存清理
|
|
18
|
+
|
|
19
|
+
核心警告:
|
|
20
|
+
- @Cacheable返回值不能使用不可变集合(List.of()、Set.of()、Map.of())
|
|
21
|
+
- 分布式锁必须在finally中释放
|
|
22
|
+
- keys()和deleteKeys()会忽略租户隔离
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Redis 缓存开发指南
|
|
26
|
+
|
|
27
|
+
> 模块位置:`ruoyi-common/ruoyi-common-redis`
|
|
28
|
+
|
|
29
|
+
## 快速索引
|
|
30
|
+
|
|
31
|
+
| 功能 | 工具类/注解 | 说明 |
|
|
32
|
+
|------|-------------|------|
|
|
33
|
+
| 对象缓存 | `RedisUtils.setCacheObject()` | 基于 Redisson |
|
|
34
|
+
| 集合缓存 | `RedisUtils.setCacheList/Set/Map()` | List/Set/Map 操作 |
|
|
35
|
+
| Spring Cache | `CacheUtils.get/put/evict()` | Spring Cache 封装 |
|
|
36
|
+
| 缓存注解 | `@Cacheable/@CachePut/@CacheEvict` | 声明式缓存 |
|
|
37
|
+
| 分布式锁 | Redisson 原生 | 需自行注入 RedissonClient |
|
|
38
|
+
| 限流控制 | `RedisUtils.rateLimiter()` | 基于 Redisson |
|
|
39
|
+
| 原子操作 | `RedisUtils.incrAtomicValue()` | 原子递增/递减 |
|
|
40
|
+
| 发布订阅 | `RedisUtils.publish/subscribe()` | 消息通信 |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 一、RedisUtils 工具类
|
|
45
|
+
|
|
46
|
+
```java
|
|
47
|
+
import org.dromara.common.redis.utils.RedisUtils;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 1.1 基础缓存操作
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
// 永不过期
|
|
54
|
+
RedisUtils.setCacheObject("user:123", userObj);
|
|
55
|
+
|
|
56
|
+
// 设置过期时间
|
|
57
|
+
RedisUtils.setCacheObject("user:123", userObj, Duration.ofMinutes(30));
|
|
58
|
+
|
|
59
|
+
// 保留原有TTL(需要Redis 6.0+)
|
|
60
|
+
RedisUtils.setCacheObject("user:123", userObj, true);
|
|
61
|
+
|
|
62
|
+
// 仅当key不存在时设置
|
|
63
|
+
boolean success = RedisUtils.setObjectIfAbsent("user:123", userObj, Duration.ofMinutes(30));
|
|
64
|
+
|
|
65
|
+
// 仅当key存在时设置
|
|
66
|
+
boolean success = RedisUtils.setObjectIfExists("user:123", userObj, Duration.ofMinutes(30));
|
|
67
|
+
|
|
68
|
+
// 获取缓存
|
|
69
|
+
User user = RedisUtils.getCacheObject("user:123");
|
|
70
|
+
|
|
71
|
+
// 获取剩余存活时间(毫秒)
|
|
72
|
+
long ttl = RedisUtils.getTimeToLive("user:123");
|
|
73
|
+
// 返回值:-1表示永不过期,-2表示key不存在
|
|
74
|
+
|
|
75
|
+
// 删除单个缓存
|
|
76
|
+
boolean deleted = RedisUtils.deleteObject("user:123");
|
|
77
|
+
|
|
78
|
+
// 批量删除
|
|
79
|
+
RedisUtils.deleteObject(Arrays.asList("user:123", "user:456"));
|
|
80
|
+
|
|
81
|
+
// 检查缓存是否存在
|
|
82
|
+
boolean exists = RedisUtils.isExistsObject("user:123");
|
|
83
|
+
|
|
84
|
+
// 设置过期时间
|
|
85
|
+
RedisUtils.expire("user:123", Duration.ofMinutes(30));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 1.2 集合操作
|
|
89
|
+
|
|
90
|
+
#### List 操作
|
|
91
|
+
|
|
92
|
+
```java
|
|
93
|
+
// 缓存List数据
|
|
94
|
+
List<String> dataList = new ArrayList<>();
|
|
95
|
+
dataList.add("item1");
|
|
96
|
+
dataList.add("item2");
|
|
97
|
+
RedisUtils.setCacheList("myList", dataList);
|
|
98
|
+
|
|
99
|
+
// 向List追加单个数据
|
|
100
|
+
RedisUtils.addCacheList("myList", "item3");
|
|
101
|
+
|
|
102
|
+
// 获取完整List
|
|
103
|
+
List<String> result = RedisUtils.getCacheList("myList");
|
|
104
|
+
|
|
105
|
+
// 获取指定范围数据
|
|
106
|
+
List<String> range = RedisUtils.getCacheListRange("myList", 0, 10);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Set 操作
|
|
110
|
+
|
|
111
|
+
```java
|
|
112
|
+
// 缓存Set数据
|
|
113
|
+
Set<String> dataSet = new HashSet<>();
|
|
114
|
+
dataSet.add("value1");
|
|
115
|
+
dataSet.add("value2");
|
|
116
|
+
RedisUtils.setCacheSet("mySet", dataSet);
|
|
117
|
+
|
|
118
|
+
// 向Set追加单个数据
|
|
119
|
+
boolean added = RedisUtils.addCacheSet("mySet", "value3");
|
|
120
|
+
|
|
121
|
+
// 获取Set数据
|
|
122
|
+
Set<String> result = RedisUtils.getCacheSet("mySet");
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Map 操作
|
|
126
|
+
|
|
127
|
+
```java
|
|
128
|
+
// 缓存Map数据
|
|
129
|
+
Map<String, Object> dataMap = new HashMap<>();
|
|
130
|
+
dataMap.put("key1", "value1");
|
|
131
|
+
dataMap.put("key2", "value2");
|
|
132
|
+
RedisUtils.setCacheMap("myMap", dataMap);
|
|
133
|
+
|
|
134
|
+
// 设置Map中的单个值
|
|
135
|
+
RedisUtils.setCacheMapValue("myMap", "key3", "value3");
|
|
136
|
+
|
|
137
|
+
// 获取Map中的单个值
|
|
138
|
+
String value = RedisUtils.getCacheMapValue("myMap", "key1");
|
|
139
|
+
|
|
140
|
+
// 获取完整Map
|
|
141
|
+
Map<String, Object> result = RedisUtils.getCacheMap("myMap");
|
|
142
|
+
|
|
143
|
+
// 获取Map的所有key
|
|
144
|
+
Set<String> keys = RedisUtils.getCacheMapKeySet("myMap");
|
|
145
|
+
|
|
146
|
+
// 删除Map中的单个值
|
|
147
|
+
Object deleted = RedisUtils.delCacheMapValue("myMap", "key1");
|
|
148
|
+
|
|
149
|
+
// 批量删除Map中的多个值
|
|
150
|
+
RedisUtils.delMultiCacheMapValue("myMap", new HashSet<>(Arrays.asList("key1", "key2")));
|
|
151
|
+
|
|
152
|
+
// 获取Map中的多个值
|
|
153
|
+
Map<String, Object> values = RedisUtils.getMultiCacheMapValue("myMap",
|
|
154
|
+
new HashSet<>(Arrays.asList("key1", "key2")));
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 1.3 发布订阅
|
|
158
|
+
|
|
159
|
+
```java
|
|
160
|
+
// 发布消息到指定频道
|
|
161
|
+
RedisUtils.publish("notification:channel", messageObj);
|
|
162
|
+
|
|
163
|
+
// 发布消息并自定义处理
|
|
164
|
+
RedisUtils.publish("notification:channel", messageObj, msg -> {
|
|
165
|
+
log.info("消息已发布: {}", msg);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 订阅频道接收消息
|
|
169
|
+
RedisUtils.subscribe("notification:channel", MessageDTO.class, msg -> {
|
|
170
|
+
log.info("收到消息: {}", msg);
|
|
171
|
+
// 处理消息逻辑
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 1.4 限流控制
|
|
176
|
+
|
|
177
|
+
```java
|
|
178
|
+
import org.redisson.api.RateType;
|
|
179
|
+
|
|
180
|
+
// 限流控制(默认无超时)
|
|
181
|
+
// 允许每10秒内最多100个请求
|
|
182
|
+
long remaining = RedisUtils.rateLimiter("api:limit:user:123",
|
|
183
|
+
RateType.OVERALL, 100, 10);
|
|
184
|
+
if (remaining == -1) {
|
|
185
|
+
throw new ServiceException("请求过于频繁,请稍后再试");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 限流控制(带超时)
|
|
189
|
+
// 允许每10秒内最多100个请求,超时时间5秒
|
|
190
|
+
long remaining = RedisUtils.rateLimiter("api:limit:user:123",
|
|
191
|
+
RateType.OVERALL, 100, 10, 5);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 1.5 原子操作
|
|
195
|
+
|
|
196
|
+
```java
|
|
197
|
+
// 设置原子长整型值
|
|
198
|
+
RedisUtils.setAtomicValue("counter:view", 0L);
|
|
199
|
+
|
|
200
|
+
// 获取原子长整型值
|
|
201
|
+
long value = RedisUtils.getAtomicValue("counter:view");
|
|
202
|
+
|
|
203
|
+
// 原子递增
|
|
204
|
+
long newValue = RedisUtils.incrAtomicValue("counter:view");
|
|
205
|
+
|
|
206
|
+
// 原子递减
|
|
207
|
+
long newValue = RedisUtils.decrAtomicValue("counter:view");
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 1.6 Key 操作
|
|
211
|
+
|
|
212
|
+
```java
|
|
213
|
+
// 获取匹配模式的所有key(注意:会忽略租户隔离)
|
|
214
|
+
Collection<String> keys = RedisUtils.keys("user:*");
|
|
215
|
+
|
|
216
|
+
// 通过扫描参数获取key列表
|
|
217
|
+
KeysScanOptions options = KeysScanOptions.defaults()
|
|
218
|
+
.pattern("user:*")
|
|
219
|
+
.chunkSize(1000)
|
|
220
|
+
.limit(100);
|
|
221
|
+
Collection<String> keys = RedisUtils.keys(options);
|
|
222
|
+
|
|
223
|
+
// 按模式批量删除key(注意:会忽略租户隔离)
|
|
224
|
+
RedisUtils.deleteKeys("temp:*");
|
|
225
|
+
|
|
226
|
+
// 检查key是否存在
|
|
227
|
+
Boolean exists = RedisUtils.hasKey("user:123");
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 1.7 分布式锁(需自行注入 RedissonClient)
|
|
231
|
+
|
|
232
|
+
> ⚠️ **注意**:`RedisUtils` 不提供 `getLock()` 方法,需要直接注入 `RedissonClient` 使用。
|
|
233
|
+
|
|
234
|
+
```java
|
|
235
|
+
import org.redisson.api.RLock;
|
|
236
|
+
import org.redisson.api.RedissonClient;
|
|
237
|
+
|
|
238
|
+
@Service
|
|
239
|
+
@RequiredArgsConstructor
|
|
240
|
+
public class OrderServiceImpl implements IOrderService {
|
|
241
|
+
|
|
242
|
+
private final RedissonClient redissonClient; // 直接注入
|
|
243
|
+
|
|
244
|
+
public void processOrder(Long orderId) {
|
|
245
|
+
// 获取锁对象
|
|
246
|
+
RLock lock = redissonClient.getLock("lock:order:" + orderId);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// 尝试加锁,最多等待10秒,锁定30秒后自动释放
|
|
250
|
+
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
|
|
251
|
+
if (locked) {
|
|
252
|
+
try {
|
|
253
|
+
// 执行业务逻辑
|
|
254
|
+
doProcess(orderId);
|
|
255
|
+
} finally {
|
|
256
|
+
// 释放锁(必须在finally中!)
|
|
257
|
+
lock.unlock();
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
throw new ServiceException("获取锁失败,请稍后重试");
|
|
261
|
+
}
|
|
262
|
+
} catch (InterruptedException e) {
|
|
263
|
+
Thread.currentThread().interrupt();
|
|
264
|
+
throw new ServiceException("获取锁被中断");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 1.8 监听机制
|
|
271
|
+
|
|
272
|
+
```java
|
|
273
|
+
import org.redisson.api.listener.ExpiredObjectListener;
|
|
274
|
+
import org.redisson.api.listener.DeletedObjectListener;
|
|
275
|
+
|
|
276
|
+
// 注册过期监听器(需要开启Redis的notify-keyspace-events配置)
|
|
277
|
+
RedisUtils.addObjectListener("user:123", (ExpiredObjectListener) name -> {
|
|
278
|
+
log.info("缓存已过期: {}", name);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// 注册删除监听器
|
|
282
|
+
RedisUtils.addObjectListener("user:123", (DeletedObjectListener) name -> {
|
|
283
|
+
log.info("缓存已删除: {}", name);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// 注册List/Set/Map监听器(同样使用具体子接口)
|
|
287
|
+
RedisUtils.addListListener("myList", (ExpiredObjectListener) name -> { ... });
|
|
288
|
+
RedisUtils.addSetListener("mySet", (DeletedObjectListener) name -> { ... });
|
|
289
|
+
RedisUtils.addMapListener("myMap", (ExpiredObjectListener) name -> { ... });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 二、CacheUtils 工具类
|
|
295
|
+
|
|
296
|
+
CacheUtils 提供了统一的 Spring Cache 操作接口。
|
|
297
|
+
|
|
298
|
+
```java
|
|
299
|
+
import org.dromara.common.redis.utils.CacheUtils;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 2.1 基本操作
|
|
303
|
+
|
|
304
|
+
```java
|
|
305
|
+
// 保存缓存值
|
|
306
|
+
CacheUtils.put("userCache", "user:123", userObj);
|
|
307
|
+
|
|
308
|
+
// 获取缓存值
|
|
309
|
+
User user = CacheUtils.get("userCache", "user:123");
|
|
310
|
+
|
|
311
|
+
// 删除指定缓存项
|
|
312
|
+
CacheUtils.evict("userCache", "user:123");
|
|
313
|
+
|
|
314
|
+
// 清空指定缓存组的所有数据
|
|
315
|
+
CacheUtils.clear("userCache");
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 2.2 使用场景
|
|
319
|
+
|
|
320
|
+
```java
|
|
321
|
+
@Service
|
|
322
|
+
public class UserServiceImpl implements IUserService {
|
|
323
|
+
|
|
324
|
+
public User getUserById(Long userId) {
|
|
325
|
+
// 先从缓存获取
|
|
326
|
+
User user = CacheUtils.get(CacheNames.SYS_USER_NAME, userId);
|
|
327
|
+
if (user != null) {
|
|
328
|
+
return user;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 缓存未命中,从数据库查询
|
|
332
|
+
user = userMapper.selectById(userId);
|
|
333
|
+
|
|
334
|
+
// 存入缓存
|
|
335
|
+
if (user != null) {
|
|
336
|
+
CacheUtils.put(CacheNames.SYS_USER_NAME, userId, user);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return user;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public void updateUser(User user) {
|
|
343
|
+
// 更新数据库
|
|
344
|
+
userMapper.updateById(user);
|
|
345
|
+
|
|
346
|
+
// 清除缓存
|
|
347
|
+
CacheUtils.evict(CacheNames.SYS_USER_NAME, user.getUserId());
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 三、CacheNames 缓存名称常量
|
|
355
|
+
|
|
356
|
+
> 位置:`ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java`
|
|
357
|
+
|
|
358
|
+
### 3.1 命名格式
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
cacheNames#ttl#maxIdleTime#maxSize#local
|
|
362
|
+
|
|
363
|
+
- ttl: 过期时间(0表示不过期,默认0)
|
|
364
|
+
- maxIdleTime: 最大空闲时间(LRU清理,0表示不检测,默认0)
|
|
365
|
+
- maxSize: 最大长度(LRU清理,0表示无限,默认0)
|
|
366
|
+
- local: 本地缓存(1开启,0关闭,默认1)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 3.2 格式示例
|
|
370
|
+
|
|
371
|
+
```java
|
|
372
|
+
// 60秒过期
|
|
373
|
+
"test#60s"
|
|
374
|
+
|
|
375
|
+
// 不过期,60秒空闲清理
|
|
376
|
+
"test#0#60s"
|
|
377
|
+
|
|
378
|
+
// 不过期,1分钟空闲清理,最大1000条
|
|
379
|
+
"test#0#1m#1000"
|
|
380
|
+
|
|
381
|
+
// 1小时过期,不检测空闲,最大500条
|
|
382
|
+
"test#1h#0#500"
|
|
383
|
+
|
|
384
|
+
// 1小时过期,不检测空闲,最大500条,关闭本地缓存
|
|
385
|
+
"test#1h#0#500#0"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 3.3 已定义常量
|
|
389
|
+
|
|
390
|
+
```java
|
|
391
|
+
public interface CacheNames {
|
|
392
|
+
|
|
393
|
+
/** 演示案例:60秒过期,10分钟空闲,最大20条 */
|
|
394
|
+
String DEMO_CACHE = "demo:cache#60s#10m#20";
|
|
395
|
+
|
|
396
|
+
/** 系统配置 */
|
|
397
|
+
String SYS_CONFIG = "sys_config";
|
|
398
|
+
|
|
399
|
+
/** 数据字典 */
|
|
400
|
+
String SYS_DICT = "sys_dict";
|
|
401
|
+
|
|
402
|
+
/** 数据字典类型 */
|
|
403
|
+
String SYS_DICT_TYPE = "sys_dict_type";
|
|
404
|
+
|
|
405
|
+
/** 租户(30天过期,全局Key) */
|
|
406
|
+
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
|
|
407
|
+
|
|
408
|
+
/** 客户端(30天过期,全局Key) */
|
|
409
|
+
String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
|
|
410
|
+
|
|
411
|
+
/** 用户账户(30天过期) */
|
|
412
|
+
String SYS_USER_NAME = "sys_user_name#30d";
|
|
413
|
+
|
|
414
|
+
/** 用户名称(30天过期) */
|
|
415
|
+
String SYS_NICKNAME = "sys_nickname#30d";
|
|
416
|
+
|
|
417
|
+
/** 部门(30天过期) */
|
|
418
|
+
String SYS_DEPT = "sys_dept#30d";
|
|
419
|
+
|
|
420
|
+
/** OSS内容(30天过期) */
|
|
421
|
+
String SYS_OSS = "sys_oss#30d";
|
|
422
|
+
|
|
423
|
+
/** 角色自定义权限(30天过期) */
|
|
424
|
+
String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
|
|
425
|
+
|
|
426
|
+
/** 部门及以下权限(30天过期) */
|
|
427
|
+
String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
|
|
428
|
+
|
|
429
|
+
/** OSS配置(全局Key) */
|
|
430
|
+
String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
|
|
431
|
+
|
|
432
|
+
/** 在线用户 */
|
|
433
|
+
String ONLINE_TOKEN = "online_tokens";
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## 四、缓存注解使用规范
|
|
440
|
+
|
|
441
|
+
### 4.1 @Cacheable - 查询缓存
|
|
442
|
+
|
|
443
|
+
```java
|
|
444
|
+
import org.springframework.cache.annotation.Cacheable;
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 根据字典类型查询字典数据
|
|
448
|
+
*/
|
|
449
|
+
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
450
|
+
@Override
|
|
451
|
+
public List<SysDictDataVo> listDictDataByType(String dictType) {
|
|
452
|
+
if (StringUtils.isBlank(dictType)) {
|
|
453
|
+
return Collections.emptyList();
|
|
454
|
+
}
|
|
455
|
+
List<SysDictData> dictDataList = dictDataMapper.selectByType(dictType);
|
|
456
|
+
if (CollUtil.isEmpty(dictDataList)) {
|
|
457
|
+
return Collections.emptyList();
|
|
458
|
+
}
|
|
459
|
+
return MapstructUtils.convert(dictDataList, SysDictDataVo.class);
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 4.2 @CachePut - 更新缓存
|
|
464
|
+
|
|
465
|
+
```java
|
|
466
|
+
import org.springframework.cache.annotation.CachePut;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 新增字典类型
|
|
470
|
+
*/
|
|
471
|
+
@CachePut(cacheNames = CacheNames.SYS_DICT, key = "#bo.dictType")
|
|
472
|
+
@Override
|
|
473
|
+
public List<SysDictDataVo> insertDictType(SysDictTypeBo bo) {
|
|
474
|
+
SysDictType dictType = MapstructUtils.convert(bo, SysDictType.class);
|
|
475
|
+
boolean success = dictTypeMapper.insert(dictType) > 0;
|
|
476
|
+
if (success) {
|
|
477
|
+
// 新增类型下无数据,返回空列表防止缓存穿透
|
|
478
|
+
return Collections.emptyList();
|
|
479
|
+
}
|
|
480
|
+
throw new ServiceException("新增字典类型失败");
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 4.3 @CacheEvict - 清除缓存
|
|
485
|
+
|
|
486
|
+
```java
|
|
487
|
+
import org.springframework.cache.annotation.CacheEvict;
|
|
488
|
+
|
|
489
|
+
// 清除单个缓存
|
|
490
|
+
@CacheEvict(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
491
|
+
public void deleteDictType(String dictType) {
|
|
492
|
+
dictTypeMapper.deleteByDictType(dictType);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 清除整个缓存组
|
|
496
|
+
@CacheEvict(cacheNames = CacheNames.SYS_DICT, allEntries = true)
|
|
497
|
+
public void clearAllDictCache() {
|
|
498
|
+
// 清空所有字典缓存
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 4.4 ⚠️ 重要:返回值不能使用不可变集合
|
|
503
|
+
|
|
504
|
+
```java
|
|
505
|
+
// ❌ 错误:使用 List.of() 会导致缓存序列化失败
|
|
506
|
+
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
507
|
+
public List<SysDictDataVo> listDictDataByType(String dictType) {
|
|
508
|
+
return List.of(data1, data2); // ❌ 禁止!
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ❌ 错误:使用 Set.of()
|
|
512
|
+
@Cacheable(cacheNames = CacheNames.SYS_USER_NAME, key = "#userId")
|
|
513
|
+
public Set<String> getUserRoles(Long userId) {
|
|
514
|
+
return Set.of("admin", "user"); // ❌ 禁止!
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ❌ 错误:使用 Map.of()
|
|
518
|
+
@Cacheable(cacheNames = CacheNames.SYS_CONFIG, key = "#configKey")
|
|
519
|
+
public Map<String, Object> getConfig(String configKey) {
|
|
520
|
+
return Map.of("key1", "value1"); // ❌ 禁止!
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ✅ 正确:使用可变集合
|
|
524
|
+
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
525
|
+
public List<SysDictDataVo> listDictDataByType(String dictType) {
|
|
526
|
+
List<SysDictDataVo> result = new ArrayList<>();
|
|
527
|
+
result.add(data1);
|
|
528
|
+
result.add(data2);
|
|
529
|
+
return result; // ✅ 正确
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## 五、分布式锁最佳实践
|
|
536
|
+
|
|
537
|
+
> ⚠️ **重要**:`RedisUtils` 不提供分布式锁方法,需要直接注入 `RedissonClient` 使用。
|
|
538
|
+
|
|
539
|
+
### 5.1 防止重复提交
|
|
540
|
+
|
|
541
|
+
```java
|
|
542
|
+
@Service
|
|
543
|
+
@RequiredArgsConstructor
|
|
544
|
+
public class OrderServiceImpl implements IOrderService {
|
|
545
|
+
|
|
546
|
+
private final RedissonClient redissonClient; // 直接注入
|
|
547
|
+
private final OrderMapper orderMapper;
|
|
548
|
+
|
|
549
|
+
public void submitOrder(OrderBo orderBo) {
|
|
550
|
+
String lockKey = "lock:submit:order:" + LoginHelper.getUserId();
|
|
551
|
+
RLock lock = redissonClient.getLock(lockKey); // ✅ 使用 redissonClient
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
|
|
555
|
+
if (!locked) {
|
|
556
|
+
throw new ServiceException("请勿重复提交订单");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
orderMapper.insert(orderBo);
|
|
561
|
+
} finally {
|
|
562
|
+
lock.unlock();
|
|
563
|
+
}
|
|
564
|
+
} catch (InterruptedException e) {
|
|
565
|
+
Thread.currentThread().interrupt();
|
|
566
|
+
throw new ServiceException("订单提交被中断");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### 5.2 库存扣减
|
|
573
|
+
|
|
574
|
+
```java
|
|
575
|
+
@Service
|
|
576
|
+
@RequiredArgsConstructor
|
|
577
|
+
public class GoodsServiceImpl implements IGoodsService {
|
|
578
|
+
|
|
579
|
+
private final RedissonClient redissonClient; // 直接注入
|
|
580
|
+
private final GoodsMapper goodsMapper;
|
|
581
|
+
|
|
582
|
+
public void deductStock(Long goodsId, Integer quantity) {
|
|
583
|
+
String lockKey = "lock:stock:" + goodsId;
|
|
584
|
+
RLock lock = redissonClient.getLock(lockKey); // ✅ 使用 redissonClient
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
|
|
588
|
+
if (!locked) {
|
|
589
|
+
throw new ServiceException("系统繁忙,请稍后重试");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
Goods goods = goodsMapper.selectById(goodsId);
|
|
594
|
+
if (goods.getStock() < quantity) {
|
|
595
|
+
throw new ServiceException("库存不足");
|
|
596
|
+
}
|
|
597
|
+
goods.setStock(goods.getStock() - quantity);
|
|
598
|
+
goodsMapper.updateById(goods);
|
|
599
|
+
} finally {
|
|
600
|
+
lock.unlock();
|
|
601
|
+
}
|
|
602
|
+
} catch (InterruptedException e) {
|
|
603
|
+
Thread.currentThread().interrupt();
|
|
604
|
+
throw new ServiceException("库存扣减被中断");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### 5.3 分布式锁规范
|
|
611
|
+
|
|
612
|
+
1. **锁的粒度要细**:锁的范围越小越好,避免锁住不必要的代码
|
|
613
|
+
2. **设置合理的超时时间**:根据业务执行时间设置,避免死锁
|
|
614
|
+
3. **必须在 finally 中释放锁**:确保锁一定会被释放
|
|
615
|
+
4. **处理 InterruptedException**:正确处理中断异常
|
|
616
|
+
5. **锁 key 要有业务含义**:便于排查问题
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## 六、缓存 Key 命名规范
|
|
621
|
+
|
|
622
|
+
### 6.1 命名格式
|
|
623
|
+
|
|
624
|
+
```
|
|
625
|
+
{业务模块}:{功能}:{具体标识}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 6.2 命名示例
|
|
629
|
+
|
|
630
|
+
```java
|
|
631
|
+
// 用户缓存
|
|
632
|
+
"user:info:123" // 用户信息
|
|
633
|
+
"user:roles:123" // 用户角色
|
|
634
|
+
"user:permissions:123" // 用户权限
|
|
635
|
+
|
|
636
|
+
// 字典缓存
|
|
637
|
+
"dict:data:sys_user_sex" // 字典数据
|
|
638
|
+
"dict:type:sys_user_sex" // 字典类型
|
|
639
|
+
|
|
640
|
+
// 订单缓存
|
|
641
|
+
"order:info:20240101001" // 订单信息
|
|
642
|
+
"order:status:20240101001" // 订单状态
|
|
643
|
+
|
|
644
|
+
// 分布式锁
|
|
645
|
+
"lock:order:20240101001" // 订单锁
|
|
646
|
+
"lock:stock:123" // 库存锁
|
|
647
|
+
"lock:submit:order:456" // 提交锁
|
|
648
|
+
|
|
649
|
+
// 限流
|
|
650
|
+
"limit:api:user:123" // 用户API限流
|
|
651
|
+
"limit:sms:18888888888" // 短信限流
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## 七、常见问题解决方案
|
|
657
|
+
|
|
658
|
+
### 7.1 缓存穿透
|
|
659
|
+
|
|
660
|
+
**问题**:查询一个不存在的数据,缓存和数据库都没有,导致每次请求都打到数据库。
|
|
661
|
+
|
|
662
|
+
```java
|
|
663
|
+
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
664
|
+
public List<SysDictDataVo> listDictDataByType(String dictType) {
|
|
665
|
+
List<SysDictData> dictDataList = dictDataMapper.selectByType(dictType);
|
|
666
|
+
if (CollUtil.isEmpty(dictDataList)) {
|
|
667
|
+
// 返回空列表而不是null,防止缓存穿透
|
|
668
|
+
return Collections.emptyList();
|
|
669
|
+
}
|
|
670
|
+
return MapstructUtils.convert(dictDataList, SysDictDataVo.class);
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### 7.2 缓存雪崩
|
|
675
|
+
|
|
676
|
+
**问题**:大量缓存同时过期,导致请求全部打到数据库。
|
|
677
|
+
|
|
678
|
+
```java
|
|
679
|
+
// 设置随机过期时间,避免同时过期
|
|
680
|
+
long baseTime = 30 * 60; // 30分钟
|
|
681
|
+
long randomTime = ThreadLocalRandom.current().nextLong(5 * 60); // 随机0-5分钟
|
|
682
|
+
Duration duration = Duration.ofSeconds(baseTime + randomTime);
|
|
683
|
+
RedisUtils.setCacheObject("user:info:" + userId, userInfo, duration);
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 7.3 缓存击穿
|
|
687
|
+
|
|
688
|
+
**问题**:热点数据过期瞬间,大量请求同时打到数据库。
|
|
689
|
+
|
|
690
|
+
```java
|
|
691
|
+
@Service
|
|
692
|
+
@RequiredArgsConstructor
|
|
693
|
+
public class UserServiceImpl implements IUserService {
|
|
694
|
+
|
|
695
|
+
private final RedissonClient redissonClient; // 直接注入
|
|
696
|
+
private final UserMapper userMapper;
|
|
697
|
+
|
|
698
|
+
public User getUserById(Long userId) {
|
|
699
|
+
String cacheKey = "user:info:" + userId;
|
|
700
|
+
|
|
701
|
+
// 先从缓存获取
|
|
702
|
+
User user = RedisUtils.getCacheObject(cacheKey);
|
|
703
|
+
if (user != null) {
|
|
704
|
+
return user;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 缓存未命中,使用分布式锁
|
|
708
|
+
String lockKey = "lock:user:" + userId;
|
|
709
|
+
RLock lock = redissonClient.getLock(lockKey); // ✅ 使用 redissonClient
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
|
|
713
|
+
if (locked) {
|
|
714
|
+
try {
|
|
715
|
+
// 双重检查
|
|
716
|
+
user = RedisUtils.getCacheObject(cacheKey);
|
|
717
|
+
if (user != null) {
|
|
718
|
+
return user;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// 从数据库查询
|
|
722
|
+
user = userMapper.selectById(userId);
|
|
723
|
+
|
|
724
|
+
// 存入缓存
|
|
725
|
+
if (user != null) {
|
|
726
|
+
RedisUtils.setCacheObject(cacheKey, user, Duration.ofMinutes(30));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return user;
|
|
730
|
+
} finally {
|
|
731
|
+
lock.unlock();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} catch (InterruptedException e) {
|
|
735
|
+
Thread.currentThread().interrupt();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 获取锁失败,直接查询数据库
|
|
739
|
+
return userMapper.selectById(userId);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### 7.4 缓存与数据库一致性
|
|
745
|
+
|
|
746
|
+
```java
|
|
747
|
+
// 方案1:先更新数据库,再删除缓存(推荐)
|
|
748
|
+
@Transactional(rollbackFor = Exception.class)
|
|
749
|
+
public void updateUser(UserBo userBo) {
|
|
750
|
+
User user = MapstructUtils.convert(userBo, User.class);
|
|
751
|
+
userMapper.updateById(user);
|
|
752
|
+
|
|
753
|
+
// 删除缓存
|
|
754
|
+
RedisUtils.deleteObject("user:info:" + user.getUserId());
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// 方案2:使用 @CachePut 更新缓存
|
|
758
|
+
@CachePut(cacheNames = CacheNames.SYS_USER_NAME, key = "#result.userId")
|
|
759
|
+
@Transactional(rollbackFor = Exception.class)
|
|
760
|
+
public UserVo updateUser(UserBo userBo) {
|
|
761
|
+
User user = MapstructUtils.convert(userBo, User.class);
|
|
762
|
+
userMapper.updateById(user);
|
|
763
|
+
|
|
764
|
+
User updatedUser = userMapper.selectById(user.getUserId());
|
|
765
|
+
return MapstructUtils.convert(updatedUser, UserVo.class);
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### 7.5 租户隔离问题
|
|
770
|
+
|
|
771
|
+
**问题**:`RedisUtils.keys()` 和 `RedisUtils.deleteKeys()` 会忽略租户隔离。
|
|
772
|
+
|
|
773
|
+
```java
|
|
774
|
+
// 手动拼接租户ID
|
|
775
|
+
String tenantId = LoginHelper.getTenantId();
|
|
776
|
+
String pattern = tenantId + ":user:*";
|
|
777
|
+
Collection<String> keys = RedisUtils.keys(pattern);
|
|
778
|
+
|
|
779
|
+
// 或者使用 CacheUtils,它会自动处理租户隔离
|
|
780
|
+
CacheUtils.clear(CacheNames.SYS_USER_NAME);
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## 八、核心文件位置
|
|
786
|
+
|
|
787
|
+
| 文件 | 位置 |
|
|
788
|
+
|------|------|
|
|
789
|
+
| RedisUtils | `ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java` |
|
|
790
|
+
| CacheUtils | `ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/CacheUtils.java` |
|
|
791
|
+
| CacheNames | `ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java` |
|
|
792
|
+
| GlobalConstants | `ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java` |
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## 九、快速参考
|
|
797
|
+
|
|
798
|
+
### 9.1 常用操作速查
|
|
799
|
+
|
|
800
|
+
```java
|
|
801
|
+
// 基础缓存
|
|
802
|
+
RedisUtils.setCacheObject(key, value); // 存储
|
|
803
|
+
RedisUtils.setCacheObject(key, value, Duration.ofMinutes(30)); // 存储并设置过期时间
|
|
804
|
+
Object value = RedisUtils.getCacheObject(key); // 获取
|
|
805
|
+
RedisUtils.deleteObject(key); // 删除
|
|
806
|
+
boolean exists = RedisUtils.isExistsObject(key); // 检查是否存在
|
|
807
|
+
|
|
808
|
+
// 分布式锁(需注入 RedissonClient,RedisUtils 不提供 getLock)
|
|
809
|
+
RLock lock = redissonClient.getLock(lockKey);
|
|
810
|
+
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
|
|
811
|
+
lock.unlock(); // 必须在 finally 中释放
|
|
812
|
+
|
|
813
|
+
// 限流
|
|
814
|
+
long remaining = RedisUtils.rateLimiter(key, RateType.OVERALL, 100, 10);
|
|
815
|
+
|
|
816
|
+
// 缓存注解
|
|
817
|
+
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType") // 查询缓存
|
|
818
|
+
@CachePut(cacheNames = CacheNames.SYS_DICT, key = "#bo.dictType") // 更新缓存
|
|
819
|
+
@CacheEvict(cacheNames = CacheNames.SYS_DICT, key = "#dictType") // 清除缓存
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### 9.2 注意事项
|
|
823
|
+
|
|
824
|
+
1. ⚠️ **@Cacheable 返回值不能使用不可变集合**(List.of()、Set.of()、Map.of())
|
|
825
|
+
2. ⚠️ **分布式锁必须在 finally 中释放**
|
|
826
|
+
3. ⚠️ **keys() 和 deleteKeys() 会忽略租户隔离**,需要手动拼接租户ID
|
|
827
|
+
4. ⚠️ **缓存 key 要有业务含义**,便于排查问题
|
|
828
|
+
5. ⚠️ **设置合理的过期时间**,避免缓存雪崩
|
|
829
|
+
6. ⚠️ **返回空列表而不是 null**,防止缓存穿透
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## 多项目适配说明
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
## 注意事项
|
|
837
|
+
|
|
838
|
+
- 如果需要 leniu-tengyun-core 项目的 Redis 开发规范,请使用 `leniu-redis-cache` skill
|
|
839
|
+
- leniu-tengyun-core 的 RedisUtil 工具类方法名与 RuoYi-Vue-Plus 的 RedisUtils 不同
|