ai-engineering-init 1.3.3 → 1.4.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/hooks/skill-forced-eval.js +4 -1
- package/.claude/settings.json +3 -3
- package/.claude/skills/add-skill/SKILL.md +252 -116
- package/.claude/skills/api-development/SKILL.md +83 -377
- package/.claude/skills/architecture-design/SKILL.md +138 -632
- package/.claude/skills/backend-annotations/SKILL.md +134 -506
- package/.claude/skills/banana-image/SKILL.md +10 -3
- package/.claude/skills/brainstorm/SKILL.md +103 -535
- package/.claude/skills/bug-detective/SKILL.md +147 -1097
- package/.claude/skills/bug-detective/references/error-patterns.md +242 -0
- package/.claude/skills/code-patterns/SKILL.md +116 -426
- package/.claude/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/.claude/skills/crud-development/SKILL.md +64 -304
- package/.claude/skills/data-permission/SKILL.md +105 -412
- package/.claude/skills/data-permission/references/custom-data-scope.md +90 -0
- package/.claude/skills/file-oss-management/SKILL.md +106 -714
- package/.claude/skills/file-oss-management/references/entities.md +105 -0
- package/.claude/skills/file-oss-management/references/service-impl.md +104 -0
- package/.claude/skills/leniu-api-development/SKILL.md +142 -626
- package/.claude/skills/leniu-api-development/references/real-examples.md +273 -0
- package/.claude/skills/leniu-architecture-design/SKILL.md +176 -391
- package/.claude/skills/leniu-backend-annotations/SKILL.md +132 -519
- package/.claude/skills/leniu-brainstorm/SKILL.md +132 -541
- package/.claude/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/.claude/skills/leniu-crud-development/SKILL.md +232 -938
- package/.claude/skills/leniu-crud-development/references/templates.md +597 -0
- package/.claude/skills/leniu-customization-location/SKILL.md +410 -0
- package/.claude/skills/leniu-data-permission/SKILL.md +70 -0
- package/.claude/skills/leniu-java-entity/SKILL.md +76 -590
- package/.claude/skills/leniu-java-entity/references/templates.md +237 -0
- package/.claude/skills/leniu-java-export/SKILL.md +94 -379
- package/.claude/skills/leniu-java-logging/SKILL.md +106 -709
- package/.claude/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/.claude/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/.claude/skills/leniu-java-mybatis/SKILL.md +73 -446
- package/.claude/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/.claude/skills/leniu-report-customization/SKILL.md +111 -325
- package/.claude/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/.claude/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/.claude/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/.claude/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/.claude/skills/leniu-security-guard/SKILL.md +133 -347
- package/.claude/skills/mysql-debug/SKILL.md +364 -0
- package/.claude/skills/openspec-apply-change/SKILL.md +10 -1
- package/.claude/skills/openspec-archive-change/SKILL.md +9 -1
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +9 -1
- package/.claude/skills/openspec-continue-change/SKILL.md +9 -1
- package/.claude/skills/openspec-explore/SKILL.md +10 -1
- package/.claude/skills/openspec-ff-change/SKILL.md +9 -1
- package/.claude/skills/openspec-new-change/SKILL.md +9 -1
- package/.claude/skills/openspec-onboard/SKILL.md +15 -130
- package/.claude/skills/openspec-sync-specs/SKILL.md +9 -1
- package/.claude/skills/openspec-verify-change/SKILL.md +9 -1
- package/.claude/skills/performance-doctor/SKILL.md +110 -434
- package/.claude/skills/redis-cache/SKILL.md +89 -595
- package/.claude/skills/redis-cache/references/listeners.md +23 -0
- package/.claude/skills/scheduled-jobs/SKILL.md +88 -407
- package/.claude/skills/security-guard/SKILL.md +137 -532
- package/.claude/skills/security-guard/references/encrypt-config.md +103 -0
- package/.claude/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/.claude/skills/sms-mail/SKILL.md +116 -574
- package/.claude/skills/sms-mail/references/mail-config.md +88 -0
- package/.claude/skills/sms-mail/references/sms-config.md +74 -0
- package/.claude/skills/social-login/SKILL.md +112 -514
- package/.claude/skills/social-login/references/provider-configs.md +118 -0
- package/.claude/skills/tenant-management/SKILL.md +129 -444
- package/.claude/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/.claude/skills/test-development/SKILL.md +86 -540
- package/.claude/skills/test-development/references/parameterized-examples.md +119 -0
- package/.claude/skills/utils-toolkit/SKILL.md +52 -305
- package/.claude/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/.claude/skills/websocket-sse/SKILL.md +105 -550
- package/.claude/skills/workflow-engine/SKILL.md +147 -502
- package/.codex/skills/add-skill/SKILL.md +252 -116
- package/.codex/skills/api-development/SKILL.md +172 -599
- package/.codex/skills/architecture-design/SKILL.md +138 -504
- package/.codex/skills/backend-annotations/SKILL.md +134 -496
- package/.codex/skills/banana-image/SKILL.md +10 -3
- package/.codex/skills/brainstorm/SKILL.md +103 -535
- package/.codex/skills/bug-detective/SKILL.md +147 -1097
- package/.codex/skills/bug-detective/references/error-patterns.md +242 -0
- package/.codex/skills/code-patterns/SKILL.md +120 -282
- package/.codex/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/.codex/skills/crud-development/SKILL.md +64 -292
- package/.codex/skills/data-permission/SKILL.md +108 -407
- package/.codex/skills/data-permission/references/custom-data-scope.md +90 -0
- package/.codex/skills/database-ops/SKILL.md +8 -154
- package/.codex/skills/error-handler/SKILL.md +10 -0
- package/.codex/skills/file-oss-management/SKILL.md +106 -714
- package/.codex/skills/file-oss-management/references/entities.md +105 -0
- package/.codex/skills/file-oss-management/references/service-impl.md +104 -0
- package/.codex/skills/git-workflow/SKILL.md +27 -5
- package/.codex/skills/leniu-api-development/SKILL.md +142 -626
- package/.codex/skills/leniu-api-development/references/real-examples.md +273 -0
- package/.codex/skills/leniu-architecture-design/SKILL.md +176 -391
- package/.codex/skills/leniu-backend-annotations/SKILL.md +132 -519
- package/.codex/skills/leniu-brainstorm/SKILL.md +132 -541
- package/.codex/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/.codex/skills/leniu-crud-development/SKILL.md +232 -938
- package/.codex/skills/leniu-crud-development/references/templates.md +597 -0
- package/.codex/skills/leniu-customization-location/SKILL.md +410 -0
- package/.codex/skills/leniu-data-permission/SKILL.md +70 -0
- package/.codex/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.codex/skills/leniu-java-entity/SKILL.md +76 -590
- package/.codex/skills/leniu-java-entity/references/templates.md +237 -0
- package/.codex/skills/leniu-java-export/SKILL.md +94 -379
- package/.codex/skills/leniu-java-logging/SKILL.md +106 -709
- package/.codex/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/.codex/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/.codex/skills/leniu-java-mybatis/SKILL.md +73 -446
- package/.codex/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/.codex/skills/leniu-report-customization/SKILL.md +111 -325
- package/.codex/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/.codex/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/.codex/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/.codex/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/.codex/skills/leniu-security-guard/SKILL.md +133 -347
- package/.codex/skills/mysql-debug/SKILL.md +364 -0
- package/.codex/skills/openspec-apply-change/SKILL.md +10 -1
- package/.codex/skills/openspec-archive-change/SKILL.md +9 -1
- package/.codex/skills/openspec-bulk-archive-change/SKILL.md +9 -1
- package/.codex/skills/openspec-continue-change/SKILL.md +9 -1
- package/.codex/skills/openspec-explore/SKILL.md +10 -1
- package/.codex/skills/openspec-ff-change/SKILL.md +9 -1
- package/.codex/skills/openspec-new-change/SKILL.md +9 -1
- package/.codex/skills/openspec-onboard/SKILL.md +15 -130
- package/.codex/skills/openspec-sync-specs/SKILL.md +9 -1
- package/.codex/skills/openspec-verify-change/SKILL.md +9 -1
- package/.codex/skills/performance-doctor/SKILL.md +110 -434
- package/.codex/skills/project-navigator/SKILL.md +20 -1
- package/.codex/skills/redis-cache/SKILL.md +93 -589
- package/.codex/skills/redis-cache/references/listeners.md +23 -0
- package/.codex/skills/scheduled-jobs/SKILL.md +88 -407
- package/.codex/skills/security-guard/SKILL.md +141 -527
- package/.codex/skills/security-guard/references/encrypt-config.md +103 -0
- package/.codex/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/.codex/skills/sms-mail/SKILL.md +116 -574
- package/.codex/skills/sms-mail/references/mail-config.md +88 -0
- package/.codex/skills/sms-mail/references/sms-config.md +74 -0
- package/.codex/skills/social-login/SKILL.md +112 -514
- package/.codex/skills/social-login/references/provider-configs.md +118 -0
- package/.codex/skills/store-pc/SKILL.md +258 -383
- package/.codex/skills/tenant-management/SKILL.md +129 -444
- package/.codex/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/.codex/skills/test-development/SKILL.md +86 -540
- package/.codex/skills/test-development/references/parameterized-examples.md +119 -0
- package/.codex/skills/ui-pc/SKILL.md +350 -387
- package/.codex/skills/utils-toolkit/SKILL.md +52 -283
- package/.codex/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/.codex/skills/websocket-sse/SKILL.md +105 -550
- package/.codex/skills/workflow-engine/SKILL.md +147 -502
- package/.cursor/hooks.json +3 -3
- package/.cursor/rules/skill-activation.mdc +2 -0
- package/.cursor/skills/add-skill/SKILL.md +252 -116
- package/.cursor/skills/api-development/SKILL.md +83 -377
- package/.cursor/skills/architecture-design/SKILL.md +138 -632
- package/.cursor/skills/backend-annotations/SKILL.md +134 -506
- package/.cursor/skills/banana-image/SKILL.md +10 -3
- package/.cursor/skills/brainstorm/SKILL.md +103 -535
- package/.cursor/skills/bug-detective/SKILL.md +147 -1097
- package/.cursor/skills/bug-detective/references/error-patterns.md +242 -0
- package/.cursor/skills/code-patterns/SKILL.md +116 -426
- package/.cursor/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/.cursor/skills/crud-development/SKILL.md +64 -304
- package/.cursor/skills/data-permission/SKILL.md +105 -412
- package/.cursor/skills/data-permission/references/custom-data-scope.md +90 -0
- package/.cursor/skills/file-oss-management/SKILL.md +106 -714
- package/.cursor/skills/file-oss-management/references/entities.md +105 -0
- package/.cursor/skills/file-oss-management/references/service-impl.md +104 -0
- package/.cursor/skills/git-workflow/SKILL.md +27 -5
- package/.cursor/skills/leniu-api-development/SKILL.md +142 -626
- package/.cursor/skills/leniu-api-development/references/real-examples.md +273 -0
- package/.cursor/skills/leniu-architecture-design/SKILL.md +176 -391
- package/.cursor/skills/leniu-backend-annotations/SKILL.md +132 -519
- package/.cursor/skills/leniu-brainstorm/SKILL.md +132 -541
- package/.cursor/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/.cursor/skills/leniu-crud-development/SKILL.md +232 -938
- package/.cursor/skills/leniu-crud-development/references/templates.md +597 -0
- package/.cursor/skills/leniu-customization-location/SKILL.md +410 -0
- package/.cursor/skills/leniu-data-permission/SKILL.md +70 -0
- package/.cursor/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.cursor/skills/leniu-java-entity/SKILL.md +76 -590
- package/.cursor/skills/leniu-java-entity/references/templates.md +237 -0
- package/.cursor/skills/leniu-java-export/SKILL.md +94 -379
- package/.cursor/skills/leniu-java-logging/SKILL.md +106 -709
- package/.cursor/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/.cursor/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/.cursor/skills/leniu-java-mybatis/SKILL.md +73 -446
- package/.cursor/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/.cursor/skills/leniu-report-customization/SKILL.md +111 -325
- package/.cursor/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/.cursor/skills/leniu-report-standard-customization/SKILL.md +328 -0
- package/.cursor/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/.cursor/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/.cursor/skills/leniu-security-guard/SKILL.md +133 -347
- package/.cursor/skills/mysql-debug/SKILL.md +364 -0
- package/.cursor/skills/openspec-apply-change/SKILL.md +10 -1
- package/.cursor/skills/openspec-archive-change/SKILL.md +9 -1
- package/.cursor/skills/openspec-bulk-archive-change/SKILL.md +9 -1
- package/.cursor/skills/openspec-continue-change/SKILL.md +9 -1
- package/.cursor/skills/openspec-explore/SKILL.md +10 -1
- package/.cursor/skills/openspec-ff-change/SKILL.md +9 -1
- package/.cursor/skills/openspec-new-change/SKILL.md +9 -1
- package/.cursor/skills/openspec-onboard/SKILL.md +15 -130
- package/.cursor/skills/openspec-sync-specs/SKILL.md +9 -1
- package/.cursor/skills/openspec-verify-change/SKILL.md +9 -1
- package/.cursor/skills/performance-doctor/SKILL.md +110 -434
- package/.cursor/skills/redis-cache/SKILL.md +89 -595
- package/.cursor/skills/redis-cache/references/listeners.md +23 -0
- package/.cursor/skills/scheduled-jobs/SKILL.md +88 -407
- package/.cursor/skills/security-guard/SKILL.md +137 -532
- package/.cursor/skills/security-guard/references/encrypt-config.md +103 -0
- package/.cursor/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/.cursor/skills/sms-mail/SKILL.md +116 -574
- package/.cursor/skills/sms-mail/references/mail-config.md +88 -0
- package/.cursor/skills/sms-mail/references/sms-config.md +74 -0
- package/.cursor/skills/social-login/SKILL.md +112 -514
- package/.cursor/skills/social-login/references/provider-configs.md +118 -0
- package/.cursor/skills/tenant-management/SKILL.md +129 -444
- package/.cursor/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/.cursor/skills/test-development/SKILL.md +86 -540
- package/.cursor/skills/test-development/references/parameterized-examples.md +119 -0
- package/.cursor/skills/utils-toolkit/SKILL.md +52 -305
- package/.cursor/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/.cursor/skills/websocket-sse/SKILL.md +105 -550
- package/.cursor/skills/workflow-engine/SKILL.md +147 -502
- package/AGENTS.md +1 -0
- package/package.json +1 -1
|
@@ -9,16 +9,14 @@ description: |
|
|
|
9
9
|
- 实现分布式锁
|
|
10
10
|
- 实现接口限流
|
|
11
11
|
- Redis发布订阅
|
|
12
|
-
- 缓存穿透/雪崩/击穿问题
|
|
13
12
|
- 缓存key设计和命名
|
|
14
|
-
- 缓存过期时间设置
|
|
15
13
|
- 缓存清理和刷新
|
|
16
14
|
|
|
17
|
-
触发词:Redis、缓存、Cache、@Cacheable、@CacheEvict、@CachePut、RedisUtils、CacheUtils、分布式锁、RLock、限流、RateLimiter
|
|
15
|
+
触发词:Redis、缓存、Cache、@Cacheable、@CacheEvict、@CachePut、RedisUtils、CacheUtils、分布式锁、RLock、限流、RateLimiter、发布订阅、缓存key、缓存过期
|
|
18
16
|
|
|
19
17
|
核心警告:
|
|
20
18
|
- @Cacheable返回值不能使用不可变集合(List.of()、Set.of()、Map.of())
|
|
21
|
-
- 分布式锁必须在finally
|
|
19
|
+
- 分布式锁必须在finally中释放,且需注入RedissonClient(RedisUtils无getLock)
|
|
22
20
|
- keys()和deleteKeys()会忽略租户隔离
|
|
23
21
|
---
|
|
24
22
|
|
|
@@ -34,7 +32,7 @@ description: |
|
|
|
34
32
|
| 集合缓存 | `RedisUtils.setCacheList/Set/Map()` | List/Set/Map 操作 |
|
|
35
33
|
| Spring Cache | `CacheUtils.get/put/evict()` | Spring Cache 封装 |
|
|
36
34
|
| 缓存注解 | `@Cacheable/@CachePut/@CacheEvict` | 声明式缓存 |
|
|
37
|
-
| 分布式锁 |
|
|
35
|
+
| 分布式锁 | `redissonClient.getLock()` | 需注入 RedissonClient |
|
|
38
36
|
| 限流控制 | `RedisUtils.rateLimiter()` | 基于 Redisson |
|
|
39
37
|
| 原子操作 | `RedisUtils.incrAtomicValue()` | 原子递增/递减 |
|
|
40
38
|
| 发布订阅 | `RedisUtils.publish/subscribe()` | 消息通信 |
|
|
@@ -50,126 +48,51 @@ import org.dromara.common.redis.utils.RedisUtils;
|
|
|
50
48
|
### 1.1 基础缓存操作
|
|
51
49
|
|
|
52
50
|
```java
|
|
53
|
-
// 永不过期
|
|
54
|
-
RedisUtils.setCacheObject("user:123", userObj);
|
|
51
|
+
RedisUtils.setCacheObject("user:123", userObj); // 永不过期
|
|
52
|
+
RedisUtils.setCacheObject("user:123", userObj, Duration.ofMinutes(30)); // 带过期时间
|
|
53
|
+
RedisUtils.setCacheObject("user:123", userObj, true); // 保留原有TTL(Redis 6.0+)
|
|
54
|
+
boolean ok = RedisUtils.setObjectIfAbsent("user:123", userObj, Duration.ofMinutes(30)); // 不存在时设置
|
|
55
|
+
boolean ok = RedisUtils.setObjectIfExists("user:123", userObj, Duration.ofMinutes(30)); // 存在时设置
|
|
55
56
|
|
|
56
|
-
//
|
|
57
|
-
RedisUtils.
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
RedisUtils.
|
|
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));
|
|
57
|
+
User user = RedisUtils.getCacheObject("user:123"); // 获取
|
|
58
|
+
long ttl = RedisUtils.getTimeToLive("user:123"); // TTL毫秒(-1永不过期,-2不存在)
|
|
59
|
+
boolean deleted = RedisUtils.deleteObject("user:123"); // 删除
|
|
60
|
+
RedisUtils.deleteObject(Arrays.asList("user:123", "user:456")); // 批量删除
|
|
61
|
+
boolean exists = RedisUtils.isExistsObject("user:123"); // 是否存在
|
|
62
|
+
RedisUtils.expire("user:123", Duration.ofMinutes(30)); // 设置过期
|
|
86
63
|
```
|
|
87
64
|
|
|
88
65
|
### 1.2 集合操作
|
|
89
66
|
|
|
90
|
-
#### List 操作
|
|
91
|
-
|
|
92
67
|
```java
|
|
93
|
-
//
|
|
94
|
-
List<String> dataList = new ArrayList<>();
|
|
95
|
-
dataList.add("item1");
|
|
96
|
-
dataList.add("item2");
|
|
68
|
+
// List
|
|
97
69
|
RedisUtils.setCacheList("myList", dataList);
|
|
98
|
-
|
|
99
|
-
// 向List追加单个数据
|
|
100
70
|
RedisUtils.addCacheList("myList", "item3");
|
|
101
|
-
|
|
102
|
-
// 获取完整List
|
|
103
71
|
List<String> result = RedisUtils.getCacheList("myList");
|
|
104
|
-
|
|
105
|
-
// 获取指定范围数据
|
|
106
72
|
List<String> range = RedisUtils.getCacheListRange("myList", 0, 10);
|
|
107
|
-
```
|
|
108
73
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```java
|
|
112
|
-
// 缓存Set数据
|
|
113
|
-
Set<String> dataSet = new HashSet<>();
|
|
114
|
-
dataSet.add("value1");
|
|
115
|
-
dataSet.add("value2");
|
|
74
|
+
// Set
|
|
116
75
|
RedisUtils.setCacheSet("mySet", dataSet);
|
|
117
|
-
|
|
118
|
-
// 向Set追加单个数据
|
|
119
76
|
boolean added = RedisUtils.addCacheSet("mySet", "value3");
|
|
120
|
-
|
|
121
|
-
// 获取Set数据
|
|
122
77
|
Set<String> result = RedisUtils.getCacheSet("mySet");
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
#### Map 操作
|
|
126
78
|
|
|
127
|
-
|
|
128
|
-
// 缓存Map数据
|
|
129
|
-
Map<String, Object> dataMap = new HashMap<>();
|
|
130
|
-
dataMap.put("key1", "value1");
|
|
131
|
-
dataMap.put("key2", "value2");
|
|
79
|
+
// Map
|
|
132
80
|
RedisUtils.setCacheMap("myMap", dataMap);
|
|
133
|
-
|
|
134
|
-
// 设置Map中的单个值
|
|
135
81
|
RedisUtils.setCacheMapValue("myMap", "key3", "value3");
|
|
136
|
-
|
|
137
|
-
// 获取Map中的单个值
|
|
138
82
|
String value = RedisUtils.getCacheMapValue("myMap", "key1");
|
|
139
|
-
|
|
140
|
-
// 获取完整Map
|
|
141
83
|
Map<String, Object> result = RedisUtils.getCacheMap("myMap");
|
|
142
|
-
|
|
143
|
-
// 获取Map的所有key
|
|
144
84
|
Set<String> keys = RedisUtils.getCacheMapKeySet("myMap");
|
|
145
|
-
|
|
146
|
-
// 删除Map中的单个值
|
|
147
|
-
Object deleted = RedisUtils.delCacheMapValue("myMap", "key1");
|
|
148
|
-
|
|
149
|
-
// 批量删除Map中的多个值
|
|
85
|
+
RedisUtils.delCacheMapValue("myMap", "key1");
|
|
150
86
|
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")));
|
|
87
|
+
Map<String, Object> values = RedisUtils.getMultiCacheMapValue("myMap", keySet);
|
|
155
88
|
```
|
|
156
89
|
|
|
157
90
|
### 1.3 发布订阅
|
|
158
91
|
|
|
159
92
|
```java
|
|
160
|
-
// 发布消息到指定频道
|
|
161
93
|
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
|
-
});
|
|
94
|
+
RedisUtils.publish("notification:channel", messageObj, msg -> { log.info("已发布: {}", msg); });
|
|
95
|
+
RedisUtils.subscribe("notification:channel", MessageDTO.class, msg -> { /* 处理消息 */ });
|
|
173
96
|
```
|
|
174
97
|
|
|
175
98
|
### 1.4 限流控制
|
|
@@ -177,259 +100,95 @@ RedisUtils.subscribe("notification:channel", MessageDTO.class, msg -> {
|
|
|
177
100
|
```java
|
|
178
101
|
import org.redisson.api.RateType;
|
|
179
102
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
long remaining = RedisUtils.rateLimiter("api:limit:user:123",
|
|
183
|
-
RateType.OVERALL, 100, 10);
|
|
103
|
+
// 每10秒最多100个请求
|
|
104
|
+
long remaining = RedisUtils.rateLimiter("api:limit:user:123", RateType.OVERALL, 100, 10);
|
|
184
105
|
if (remaining == -1) {
|
|
185
|
-
throw new ServiceException("
|
|
106
|
+
throw new ServiceException("请求过于频繁");
|
|
186
107
|
}
|
|
187
108
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
long remaining = RedisUtils.rateLimiter("api:limit:user:123",
|
|
191
|
-
RateType.OVERALL, 100, 10, 5);
|
|
109
|
+
// 带超时版本(5秒超时)
|
|
110
|
+
long remaining = RedisUtils.rateLimiter("api:limit:user:123", RateType.OVERALL, 100, 10, 5);
|
|
192
111
|
```
|
|
193
112
|
|
|
194
113
|
### 1.5 原子操作
|
|
195
114
|
|
|
196
115
|
```java
|
|
197
|
-
// 设置原子长整型值
|
|
198
116
|
RedisUtils.setAtomicValue("counter:view", 0L);
|
|
199
|
-
|
|
200
|
-
// 获取原子长整型值
|
|
201
117
|
long value = RedisUtils.getAtomicValue("counter:view");
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
long newValue = RedisUtils.incrAtomicValue("counter:view");
|
|
205
|
-
|
|
206
|
-
// 原子递减
|
|
207
|
-
long newValue = RedisUtils.decrAtomicValue("counter:view");
|
|
118
|
+
long newVal = RedisUtils.incrAtomicValue("counter:view");
|
|
119
|
+
long newVal = RedisUtils.decrAtomicValue("counter:view");
|
|
208
120
|
```
|
|
209
121
|
|
|
210
122
|
### 1.6 Key 操作
|
|
211
123
|
|
|
212
124
|
```java
|
|
213
|
-
//
|
|
125
|
+
// ⚠️ keys() 和 deleteKeys() 会忽略租户隔离
|
|
214
126
|
Collection<String> keys = RedisUtils.keys("user:*");
|
|
215
127
|
|
|
216
|
-
|
|
217
|
-
KeysScanOptions options = KeysScanOptions.defaults()
|
|
218
|
-
.pattern("user:*")
|
|
219
|
-
.chunkSize(1000)
|
|
220
|
-
.limit(100);
|
|
128
|
+
KeysScanOptions options = KeysScanOptions.defaults().pattern("user:*").chunkSize(1000).limit(100);
|
|
221
129
|
Collection<String> keys = RedisUtils.keys(options);
|
|
222
130
|
|
|
223
|
-
// 按模式批量删除key(注意:会忽略租户隔离)
|
|
224
131
|
RedisUtils.deleteKeys("temp:*");
|
|
225
|
-
|
|
226
|
-
// 检查key是否存在
|
|
227
132
|
Boolean exists = RedisUtils.hasKey("user:123");
|
|
228
133
|
```
|
|
229
134
|
|
|
230
|
-
### 1.7
|
|
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 监听机制
|
|
135
|
+
### 1.7 监听机制
|
|
271
136
|
|
|
272
|
-
|
|
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
|
-
```
|
|
137
|
+
详见 [references/listeners.md](references/listeners.md)
|
|
291
138
|
|
|
292
139
|
---
|
|
293
140
|
|
|
294
141
|
## 二、CacheUtils 工具类
|
|
295
142
|
|
|
296
|
-
CacheUtils 提供了统一的 Spring Cache 操作接口。
|
|
297
|
-
|
|
298
143
|
```java
|
|
299
144
|
import org.dromara.common.redis.utils.CacheUtils;
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### 2.1 基本操作
|
|
303
|
-
|
|
304
|
-
```java
|
|
305
|
-
// 保存缓存值
|
|
306
|
-
CacheUtils.put("userCache", "user:123", userObj);
|
|
307
145
|
|
|
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
|
-
}
|
|
146
|
+
CacheUtils.put("userCache", "user:123", userObj); // 保存
|
|
147
|
+
User user = CacheUtils.get("userCache", "user:123"); // 获取
|
|
148
|
+
CacheUtils.evict("userCache", "user:123"); // 删除
|
|
149
|
+
CacheUtils.clear("userCache"); // 清空整组
|
|
350
150
|
```
|
|
351
151
|
|
|
352
152
|
---
|
|
353
153
|
|
|
354
154
|
## 三、CacheNames 缓存名称常量
|
|
355
155
|
|
|
356
|
-
> 位置:`ruoyi-common/ruoyi-common-core
|
|
156
|
+
> 位置:`ruoyi-common/ruoyi-common-core/.../constant/CacheNames.java`
|
|
357
157
|
|
|
358
|
-
###
|
|
158
|
+
### 命名格式
|
|
359
159
|
|
|
360
160
|
```
|
|
361
161
|
cacheNames#ttl#maxIdleTime#maxSize#local
|
|
362
|
-
|
|
363
|
-
-
|
|
364
|
-
-
|
|
365
|
-
- maxSize: 最大长度(LRU清理,0表示无限,默认0)
|
|
162
|
+
- ttl: 过期时间(0=不过期,默认0)
|
|
163
|
+
- maxIdleTime: 最大空闲时间(0=不检测,默认0)
|
|
164
|
+
- maxSize: 最大条数(0=无限,默认0)
|
|
366
165
|
- local: 本地缓存(1开启,0关闭,默认1)
|
|
367
166
|
```
|
|
368
167
|
|
|
369
|
-
### 3.2 格式示例
|
|
370
|
-
|
|
371
168
|
```java
|
|
372
|
-
// 60秒过期
|
|
373
|
-
"test#60s"
|
|
374
|
-
|
|
375
|
-
//
|
|
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"
|
|
169
|
+
"test#60s" // 60秒过期
|
|
170
|
+
"test#0#60s" // 不过期,60秒空闲清理
|
|
171
|
+
"test#0#1m#1000" // 不过期,1分钟空闲,最大1000条
|
|
172
|
+
"test#1h#0#500#0" // 1小时过期,最大500条,关闭本地缓存
|
|
386
173
|
```
|
|
387
174
|
|
|
388
|
-
###
|
|
175
|
+
### 已定义常量
|
|
389
176
|
|
|
390
177
|
```java
|
|
391
178
|
public interface CacheNames {
|
|
392
|
-
|
|
393
|
-
/** 演示案例:60秒过期,10分钟空闲,最大20条 */
|
|
394
179
|
String DEMO_CACHE = "demo:cache#60s#10m#20";
|
|
395
|
-
|
|
396
|
-
/** 系统配置 */
|
|
397
180
|
String SYS_CONFIG = "sys_config";
|
|
398
|
-
|
|
399
|
-
/** 数据字典 */
|
|
400
181
|
String SYS_DICT = "sys_dict";
|
|
401
|
-
|
|
402
|
-
/** 数据字典类型 */
|
|
403
182
|
String SYS_DICT_TYPE = "sys_dict_type";
|
|
404
|
-
|
|
405
|
-
/** 租户(30天过期,全局Key) */
|
|
406
183
|
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
|
|
407
|
-
|
|
408
|
-
/** 客户端(30天过期,全局Key) */
|
|
409
184
|
String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
|
|
410
|
-
|
|
411
|
-
/** 用户账户(30天过期) */
|
|
412
185
|
String SYS_USER_NAME = "sys_user_name#30d";
|
|
413
|
-
|
|
414
|
-
/** 用户名称(30天过期) */
|
|
415
186
|
String SYS_NICKNAME = "sys_nickname#30d";
|
|
416
|
-
|
|
417
|
-
/** 部门(30天过期) */
|
|
418
187
|
String SYS_DEPT = "sys_dept#30d";
|
|
419
|
-
|
|
420
|
-
/** OSS内容(30天过期) */
|
|
421
188
|
String SYS_OSS = "sys_oss#30d";
|
|
422
|
-
|
|
423
|
-
/** 角色自定义权限(30天过期) */
|
|
424
189
|
String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
|
|
425
|
-
|
|
426
|
-
/** 部门及以下权限(30天过期) */
|
|
427
190
|
String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
|
|
428
|
-
|
|
429
|
-
/** OSS配置(全局Key) */
|
|
430
191
|
String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
|
|
431
|
-
|
|
432
|
-
/** 在线用户 */
|
|
433
192
|
String ONLINE_TOKEN = "online_tokens";
|
|
434
193
|
}
|
|
435
194
|
```
|
|
@@ -438,14 +197,9 @@ public interface CacheNames {
|
|
|
438
197
|
|
|
439
198
|
## 四、缓存注解使用规范
|
|
440
199
|
|
|
441
|
-
###
|
|
200
|
+
### @Cacheable - 查询缓存
|
|
442
201
|
|
|
443
202
|
```java
|
|
444
|
-
import org.springframework.cache.annotation.Cacheable;
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* 根据字典类型查询字典数据
|
|
448
|
-
*/
|
|
449
203
|
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
450
204
|
@Override
|
|
451
205
|
public List<SysDictDataVo> listDictDataByType(String dictType) {
|
|
@@ -454,112 +208,65 @@ public List<SysDictDataVo> listDictDataByType(String dictType) {
|
|
|
454
208
|
}
|
|
455
209
|
List<SysDictData> dictDataList = dictDataMapper.selectByType(dictType);
|
|
456
210
|
if (CollUtil.isEmpty(dictDataList)) {
|
|
457
|
-
return Collections.emptyList();
|
|
211
|
+
return Collections.emptyList(); // 返回空列表防止缓存穿透
|
|
458
212
|
}
|
|
459
213
|
return MapstructUtils.convert(dictDataList, SysDictDataVo.class);
|
|
460
214
|
}
|
|
461
215
|
```
|
|
462
216
|
|
|
463
|
-
###
|
|
217
|
+
### @CachePut / @CacheEvict
|
|
464
218
|
|
|
465
219
|
```java
|
|
466
|
-
import org.springframework.cache.annotation.CachePut;
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* 新增字典类型
|
|
470
|
-
*/
|
|
471
220
|
@CachePut(cacheNames = CacheNames.SYS_DICT, key = "#bo.dictType")
|
|
472
|
-
|
|
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
|
-
```
|
|
221
|
+
public List<SysDictDataVo> insertDictType(SysDictTypeBo bo) { ... }
|
|
483
222
|
|
|
484
|
-
### 4.3 @CacheEvict - 清除缓存
|
|
485
|
-
|
|
486
|
-
```java
|
|
487
|
-
import org.springframework.cache.annotation.CacheEvict;
|
|
488
|
-
|
|
489
|
-
// 清除单个缓存
|
|
490
223
|
@CacheEvict(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
|
491
|
-
public void deleteDictType(String dictType) {
|
|
492
|
-
dictTypeMapper.deleteByDictType(dictType);
|
|
493
|
-
}
|
|
224
|
+
public void deleteDictType(String dictType) { ... }
|
|
494
225
|
|
|
495
|
-
// 清除整个缓存组
|
|
496
226
|
@CacheEvict(cacheNames = CacheNames.SYS_DICT, allEntries = true)
|
|
497
|
-
public void clearAllDictCache() {
|
|
498
|
-
// 清空所有字典缓存
|
|
499
|
-
}
|
|
227
|
+
public void clearAllDictCache() { ... }
|
|
500
228
|
```
|
|
501
229
|
|
|
502
|
-
###
|
|
230
|
+
### 返回值禁止使用不可变集合
|
|
503
231
|
|
|
504
232
|
```java
|
|
505
|
-
// ❌
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
}
|
|
233
|
+
// ❌ 禁止!@Cacheable 返回值序列化失败
|
|
234
|
+
return List.of(data1, data2);
|
|
235
|
+
return Set.of("admin", "user");
|
|
236
|
+
return Map.of("key1", "value1");
|
|
522
237
|
|
|
523
238
|
// ✅ 正确:使用可变集合
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
result.add(data1);
|
|
528
|
-
result.add(data2);
|
|
529
|
-
return result; // ✅ 正确
|
|
530
|
-
}
|
|
239
|
+
List<SysDictDataVo> result = new ArrayList<>();
|
|
240
|
+
result.add(data1);
|
|
241
|
+
return result;
|
|
531
242
|
```
|
|
532
243
|
|
|
533
244
|
---
|
|
534
245
|
|
|
535
|
-
##
|
|
536
|
-
|
|
537
|
-
> ⚠️ **重要**:`RedisUtils` 不提供分布式锁方法,需要直接注入 `RedissonClient` 使用。
|
|
246
|
+
## 五、分布式锁
|
|
538
247
|
|
|
539
|
-
|
|
248
|
+
> RedisUtils **不提供** `getLock()`,需直接注入 `RedissonClient`。
|
|
540
249
|
|
|
541
250
|
```java
|
|
542
251
|
@Service
|
|
543
252
|
@RequiredArgsConstructor
|
|
544
253
|
public class OrderServiceImpl implements IOrderService {
|
|
545
254
|
|
|
546
|
-
private final RedissonClient redissonClient;
|
|
547
|
-
private final OrderMapper orderMapper;
|
|
255
|
+
private final RedissonClient redissonClient;
|
|
548
256
|
|
|
549
257
|
public void submitOrder(OrderBo orderBo) {
|
|
550
258
|
String lockKey = "lock:submit:order:" + LoginHelper.getUserId();
|
|
551
|
-
RLock lock = redissonClient.getLock(lockKey);
|
|
259
|
+
RLock lock = redissonClient.getLock(lockKey);
|
|
552
260
|
|
|
553
261
|
try {
|
|
554
262
|
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
|
|
555
263
|
if (!locked) {
|
|
556
264
|
throw new ServiceException("请勿重复提交订单");
|
|
557
265
|
}
|
|
558
|
-
|
|
559
266
|
try {
|
|
560
267
|
orderMapper.insert(orderBo);
|
|
561
268
|
} finally {
|
|
562
|
-
lock.unlock();
|
|
269
|
+
lock.unlock(); // 必须在 finally 中释放
|
|
563
270
|
}
|
|
564
271
|
} catch (InterruptedException e) {
|
|
565
272
|
Thread.currentThread().interrupt();
|
|
@@ -569,271 +276,58 @@ public class OrderServiceImpl implements IOrderService {
|
|
|
569
276
|
}
|
|
570
277
|
```
|
|
571
278
|
|
|
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
279
|
---
|
|
619
280
|
|
|
620
281
|
## 六、缓存 Key 命名规范
|
|
621
282
|
|
|
622
|
-
### 6.1 命名格式
|
|
623
|
-
|
|
624
|
-
```
|
|
625
|
-
{业务模块}:{功能}:{具体标识}
|
|
626
283
|
```
|
|
284
|
+
格式:{业务模块}:{功能}:{具体标识}
|
|
627
285
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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" // 短信限流
|
|
286
|
+
示例:
|
|
287
|
+
user:info:123 user:roles:123 user:permissions:123
|
|
288
|
+
dict:data:sys_user_sex order:info:20240101001 order:status:20240101001
|
|
289
|
+
lock:order:20240101001 lock:stock:123 lock:submit:order:456
|
|
290
|
+
limit:api:user:123 limit:sms:18888888888
|
|
652
291
|
```
|
|
653
292
|
|
|
654
293
|
---
|
|
655
294
|
|
|
656
|
-
##
|
|
657
|
-
|
|
658
|
-
### 7.1 缓存穿透
|
|
659
|
-
|
|
660
|
-
**问题**:查询一个不存在的数据,缓存和数据库都没有,导致每次请求都打到数据库。
|
|
295
|
+
## 七、租户隔离注意
|
|
661
296
|
|
|
662
297
|
```java
|
|
663
|
-
|
|
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
|
|
298
|
+
// ⚠️ keys() 和 deleteKeys() 会忽略租户隔离,需手动拼接
|
|
775
299
|
String tenantId = LoginHelper.getTenantId();
|
|
776
|
-
String
|
|
777
|
-
Collection<String> keys = RedisUtils.keys(pattern);
|
|
300
|
+
Collection<String> keys = RedisUtils.keys(tenantId + ":user:*");
|
|
778
301
|
|
|
779
|
-
//
|
|
302
|
+
// CacheUtils 会自动处理租户隔离
|
|
780
303
|
CacheUtils.clear(CacheNames.SYS_USER_NAME);
|
|
781
304
|
```
|
|
782
305
|
|
|
783
306
|
---
|
|
784
307
|
|
|
785
|
-
##
|
|
308
|
+
## 八、注意事项速查
|
|
786
309
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
310
|
+
1. **@Cacheable 返回值不能使用不可变集合**(List.of()、Set.of()、Map.of())
|
|
311
|
+
2. **分布式锁必须在 finally 中释放**
|
|
312
|
+
3. **keys() 和 deleteKeys() 会忽略租户隔离**
|
|
313
|
+
4. **返回空列表而不是 null**,防止缓存穿透
|
|
314
|
+
5. **设置随机过期时间偏移**,避免缓存雪崩
|
|
315
|
+
6. **热点数据用分布式锁 + 双重检查**,防止缓存击穿
|
|
793
316
|
|
|
794
317
|
---
|
|
795
318
|
|
|
796
|
-
##
|
|
319
|
+
## 九、核心文件位置
|
|
797
320
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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**,防止缓存穿透
|
|
321
|
+
| 文件 | 位置 |
|
|
322
|
+
|------|------|
|
|
323
|
+
| RedisUtils | `ruoyi-common/ruoyi-common-redis/.../utils/RedisUtils.java` |
|
|
324
|
+
| CacheUtils | `ruoyi-common/ruoyi-common-redis/.../utils/CacheUtils.java` |
|
|
325
|
+
| CacheNames | `ruoyi-common/ruoyi-common-core/.../constant/CacheNames.java` |
|
|
326
|
+
| GlobalConstants | `ruoyi-common/ruoyi-common-core/.../constant/GlobalConstants.java` |
|
|
830
327
|
|
|
831
328
|
---
|
|
832
329
|
|
|
833
330
|
## 多项目适配说明
|
|
834
331
|
|
|
835
|
-
|
|
836
|
-
## 注意事项
|
|
837
|
-
|
|
838
332
|
- 如果需要 leniu-tengyun-core 项目的 Redis 开发规范,请使用 `leniu-redis-cache` skill
|
|
839
333
|
- leniu-tengyun-core 的 RedisUtil 工具类方法名与 RuoYi-Vue-Plus 的 RedisUtils 不同
|