docs(cache): README 缓存章节重写为「快速上手 + 详细参考」

第十节拆为 10.1 快速上手 + 10.2 API 语义 + 10.3 三防原理 + 10.4 配置 + 10.5
约定与坑,并纳入本轮行为变更:ttl 必须 > 0、exists 排除空值标记、set(null) 用
null-ttl、key-prefix 需自带分隔符、缺 StringRedisTemplate 自动退场、配置装配期
fail-fast、抖动半开区间、降级不写缓存等。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-06-26 10:16:56 +08:00
parent 0657126f60
commit 7101a1fa65

View File

@@ -277,9 +277,13 @@ mvn test
## 十、Redis 缓存(`RedisCacheEnhanceTemplate`)
除 Stream 收发外,starter 同样提供开箱即用的**编程式缓存**,把 TTL序列化、穿透/雪崩/击穿三防封装进一个 `getOrLoad`。注入即用,无需额外注解
除 Stream 收发外,starter 同样提供开箱即用的**编程式缓存**:把 TTL 管理、fastjson 序列化、**穿透/雪崩/击穿三防**与滑动续期,全部封装进一个 `getOrLoad`**注入即用,无需任何注解**;缓存模块与 Stream 完全独立——只用缓存、不注册任何 handler 也能工作,反之亦然
### 发送/读取
> 依赖与连接配置同 Stream:引入本 starter、配好 `spring.redis.*` 即自动装配出 `RedisCacheEnhanceTemplate` Bean(在 `RedisAutoConfiguration` 之后生效)。**若上下文内没有 `StringRedisTemplate` Bean(例如自定义了多个 `RedisConnectionFactory`、或 `exclude` 了 `RedisAutoConfiguration`),缓存自动装配会整体退场而非启动失败**;此时如需缓存请自行提供 `StringRedisTemplate`。
### 10.1 快速上手
注入 `RedisCacheEnhanceTemplate`,用 `getOrLoad` 一行搞定「读缓存 → 未命中回源 → 写回」,三防自动生效:
```java
import com.njcn.middle.cache.template.RedisCacheEnhanceTemplate;
@@ -295,42 +299,73 @@ public class UserCache {
}
public User getUser(Long id) {
// 命中即返回;未命中则回源 loadFromDb 并写缓存,自动防穿透/雪崩/击穿
// 命中即返回;未命中则回源 loadFromDb 并写,自动防穿透/雪崩/击穿
// 第三参是 TTL(秒,必须 > 0);想用默认 TTL 可省略该参数
return cache.getOrLoad("user:" + id, User.class, 3600, () -> loadFromDb(id));
}
public void evict(Long id) {
cache.delete("user:" + id);
public void evictUser(Long id) {
cache.delete("user:" + id); // 失效
}
}
```
核心方法:
不配置任何 `redis-cache.*` 即用默认值(default-ttl 1 小时、抖动 10%、击穿互斥锁开启),开箱即用。需要直接读写时另有 `set` / `get` / `exists` / `expire`,见下。
| 方法 | 说明 |
### 10.2 API 一览与语义
| 方法 | 语义 / 边界 |
|---|---|
| `set(key, value[, ttlSeconds])` | 写缓存,TTL 叠加随机抖动(防雪崩);不传 TTL 用 `default-ttl-seconds` |
| `get(key, Class<T>)` / `get(key, TypeReference<T>)` | 读缓存,支持泛型;未命中或命中空值标记返回 `null`;脏数据抛 `IllegalStateException` |
| `delete(key)` / `exists(key)` | 删除 / 判存 |
| `expire(key, ttlSeconds)` | 显式续期(走抖动);key 不存在返回 `false` |
| `getOrLoad(key, type[, ttlSeconds], loader)` | 读取或回源;内置穿透(空值标记)+ 雪崩(TTL 抖动)+ 击穿(互斥锁)三防 |
| `set(key, value[, ttlSeconds])` | 写缓存(fastjson 序列化)。真实值 TTL 叠加随机抖动(防雪崩);**`value``null` 时写空值标记并改用短 `null-ttl-seconds`**;不传 `ttlSeconds``default-ttl-seconds`。**`ttlSeconds` 必须 > 0,否则抛 `IllegalArgumentException`**。 |
| `get(key, Class<T>)`<br>`get(key, TypeReference<T>)` | 读缓存,支持泛型未命中或命中空值标记返回 `null`;**脏数据(无法反序列化)→ `IllegalStateException`**(显式暴露便于排查)。 |
| `exists(key)` | 是否存在**真实值**。**空值标记与未命中均返回 `false`**(与 `get` 口径一致,读值判断而非物理 `EXISTS`)。 |
| `delete(key)` | 删除,返回是否确实删除了该 key。 |
| `expire(key, ttlSeconds)` | 显式续期(走抖动)。key 不存在 → `false`;**`ttlSeconds` 必须 > 0,否则抛 `IllegalArgumentException`**(避免传 0/负被 Redis 当成「立即删除」)。 |
| `getOrLoad(key, type, loader)`<br>`getOrLoad(key, type, ttlSeconds, loader)`<br>`getOrLoad(key, TypeReference, ttlSeconds, loader)` | 读取或回源,三防齐全。`ttlSeconds` 缺省用 `default-ttl-seconds`,**显式传入时必须 > 0**;`loader` 返回 `null` 会被缓存为空值标记(短 `null-ttl`)。 |
### 配置项(前缀 `redis-cache`,均可选)
> **两套反序列化语义**:公共 `get` 遇脏数据**抛** `IllegalStateException`(让坏数据显式暴露);`getOrLoad` 内部读到脏数据则**当未命中降级回源**并覆盖重建(自愈)。
### 10.3 三防与滑动续期(工作原理)
- **穿透**:`loader` 回源得到 `null` 时写入空值标记 + 短 `null-ttl-seconds`(不抖动);后续命中空标记直接返回 `null`,不再反复打到后端。
- **雪崩**:真实值写入时 TTL 叠加 `[0, ttl × jitter-ratio)` 的随机抖动(**半开区间,只增不减**),打散集中过期。`jitter-ratio = 0` 关闭抖动。
- **击穿**:未命中时用 `SET NX PX`(`setIfAbsent` + `lock-ttl-ms`)抢互斥锁:
- 抢到锁者先 **double-check**(再读一次,吃掉「刚有人写好」的窗口),仍未命中才回源、写回,`finally` 用 **Lua 脚本按 token 匹配**释放锁(只删自己的锁,不误删他人)。
- 未抢到锁者按 `wait-interval-ms` 自旋重读,读到值即返回;**超时(`wait-timeout-ms`)或线程被中断则降级直接 `loader.get()` 返回**。
- 关闭(`breakdown.enabled = false`)则退化为无锁直接回源。
- **滑动续期**(`sliding-expire-enabled = true`):**读命中真实值**时自动续 `default-ttl-seconds`;**空值标记不续**(保持穿透防护的短 TTL);回源新写入自带 TTL、不重复续。
> ⚠️ **降级取舍**:持锁者回源若慢于 `wait-timeout-ms`(默认 3s)或回源抛异常,等待者会各自降级回源——瞬时被放大为 N 次回源,且**降级路径不写缓存**(有意为之,优先可用性)。务必让 `loader` 回源耗时显著小于 `wait-timeout-ms`。
### 10.4 配置项(前缀 `redis-cache`,均可选)
不配即用注释中的默认值。Spring 宽松绑定,`null-ttl-seconds` / `nullTtlSeconds` 等写法等价。
```yaml
redis-cache:
key-prefix: "cache:" # 统一 key 前缀
env-isolation: false # 按环境隔离 key(开启且 env 非空 → cache:env:key)
env: ""
default-ttl-seconds: 3600 # 默认 TTL(秒)
null-ttl-seconds: 60 # 空值标记 TTL(秒,穿透防护)
jitter-ratio: 0.1 # TTL 随机抖动比例(雪崩防护;0 关闭)
sliding-expire-enabled: false # 读命中真实值自动续 default-ttl(滑动过期,空值标记不续)
key-prefix: "cache:" # 统一 key 前缀;须自带分隔符(如尾冒号),否则 cache + user → cacheuser
env-isolation: false # 按环境隔离 key:开启且 env 非空 → key 形如 cache:env:bizKey
env: "" # 环境标识,隔离开启且非空时拼入 key
default-ttl-seconds: 3600 # set/getOrLoad 不传 TTL 时的默认过期(秒);必须 > 0
null-ttl-seconds: 60 # 空值标记 TTL(秒)——穿透防护,宜短;必须 > 0
jitter-ratio: 0.1 # TTL 随机抖动比例(雪崩防护),取值 [0,1);0 关闭抖动
sliding-expire-enabled: false # 读命中真实值自动续 default-ttl(滑动过期;空值标记不续)
breakdown: # 击穿:互斥锁
enabled: true # 关闭则退化为无锁直接回源
lock-ttl-ms: 10000 # 锁持有 TTL
wait-timeout-ms: 3000 # 未抢到锁的最长等待,超时降级直接回源
wait-interval-ms: 50 # 未抢到锁的自旋重读间隔
lock-ttl-ms: 10000 # 锁持有 TTL(ms),防持锁线程崩溃死锁;必须 > 0
wait-timeout-ms: 3000 # 未抢到锁的最长等待(ms),超时降级直接回源;>= 0
wait-interval-ms: 50 # 未抢到锁的自旋重读间隔(ms);必须 > 0
```
> 上述取值在**装配时即被校验**:TTL 非正、`jitter-ratio` 越界 `[0,1)`、`lock-ttl-ms` / `wait-interval-ms` 非正等会让上下文**启动即失败(fail-fast)**,而非留到运行期才暴露。
### 10.5 必读约定与坑
- **TTL 必须为正**:`set` / `expire` / `getOrLoad`(显式 TTL)传入 `ttlSeconds ≤ 0` 一律抛 `IllegalArgumentException`。Redis 对 `EXPIRE` 非正值的语义是「立即删除 key」,框架拦在入口避免「续期变删数据」。本模板面向带 TTL 的缓存,不支持「永不过期」。
- **`key-prefix` 需自带分隔符**:key 按 `前缀 + (env + ':') + bizKey` 直接拼接,前缀不带尾分隔符会粘连(`cache` + `user:1``cacheuser:1`)。默认 `cache:` 已自带冒号。`bizKey` 不可为空白,否则抛 `IllegalArgumentException`
- **`exists` 不等于物理存在**:只在有**真实值**时返回 `true`,空值标记返回 `false`(与 `get` 一致)。
- **缺 Redis 自动退场**:无 `StringRedisTemplate` Bean 时缓存装配整体 back off、不报错;此时注入 `RedisCacheEnhanceTemplate` 会因无此 Bean 而失败。
- **脏数据两套语义**:公共 `get``IllegalStateException`;`getOrLoad` 内部当未命中降级并重建。
- **覆盖默认 Bean**:`CacheKeyBuilder` / `CacheValueCodec` / `RedisCacheEnhanceTemplate``@ConditionalOnMissingBean`,自定义同类型 Bean 即可覆盖。
> 缓存模块与 Stream 完全独立:只用缓存不注册任何 handler 也能工作;反之亦然。