缓存优化: 切换 Redis 作为缓存产品与 TTL 配置
纲要
本篇对应课程 day01 的「使用 Redis 作为缓存产品」两个环节,完成从内存缓存到分布式缓存的切换:
- 两个坐标 :
spring-boot-starter-cache(缓存抽象扩展)与spring-boot-starter-data-redis(提供RedisCacheManager) - 配置 :
spring.redis.*连接信息 +spring.cache.redis.time-to-live全局过期时间 CacheManager自动切换 :导入坐标后实现类从 5 个增加到十几个,RedisCacheManager被自动选中- 实测复现
condition的坑 :condition = "#result != null"导致缓存永不命中,必须用unless Redis中的key格式 :userCache::1502198504921124865- 验证 :
Redis客户端直接观察缓存数据,@CacheEvict删除后key同步消失
#mermaid-svg-Fq7cmAvdjnsV96zE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Fq7cmAvdjnsV96zE .error-icon{fill:#552222;}#mermaid-svg-Fq7cmAvdjnsV96zE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Fq7cmAvdjnsV96zE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Fq7cmAvdjnsV96zE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Fq7cmAvdjnsV96zE .marker.cross{stroke:#333333;}#mermaid-svg-Fq7cmAvdjnsV96zE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Fq7cmAvdjnsV96zE p{margin:0;}#mermaid-svg-Fq7cmAvdjnsV96zE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE .cluster-label text{fill:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE .cluster-label span{color:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE .cluster-label span p{background-color:transparent;}#mermaid-svg-Fq7cmAvdjnsV96zE .label text,#mermaid-svg-Fq7cmAvdjnsV96zE span{fill:#333;color:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE .node rect,#mermaid-svg-Fq7cmAvdjnsV96zE .node circle,#mermaid-svg-Fq7cmAvdjnsV96zE .node ellipse,#mermaid-svg-Fq7cmAvdjnsV96zE .node polygon,#mermaid-svg-Fq7cmAvdjnsV96zE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Fq7cmAvdjnsV96zE .rough-node .label text,#mermaid-svg-Fq7cmAvdjnsV96zE .node .label text,#mermaid-svg-Fq7cmAvdjnsV96zE .image-shape .label,#mermaid-svg-Fq7cmAvdjnsV96zE .icon-shape .label{text-anchor:middle;}#mermaid-svg-Fq7cmAvdjnsV96zE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Fq7cmAvdjnsV96zE .rough-node .label,#mermaid-svg-Fq7cmAvdjnsV96zE .node .label,#mermaid-svg-Fq7cmAvdjnsV96zE .image-shape .label,#mermaid-svg-Fq7cmAvdjnsV96zE .icon-shape .label{text-align:center;}#mermaid-svg-Fq7cmAvdjnsV96zE .node.clickable{cursor:pointer;}#mermaid-svg-Fq7cmAvdjnsV96zE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Fq7cmAvdjnsV96zE .arrowheadPath{fill:#333333;}#mermaid-svg-Fq7cmAvdjnsV96zE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Fq7cmAvdjnsV96zE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Fq7cmAvdjnsV96zE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fq7cmAvdjnsV96zE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Fq7cmAvdjnsV96zE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fq7cmAvdjnsV96zE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Fq7cmAvdjnsV96zE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Fq7cmAvdjnsV96zE .cluster text{fill:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE .cluster span{color:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Fq7cmAvdjnsV96zE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Fq7cmAvdjnsV96zE rect.text{fill:none;stroke-width:0;}#mermaid-svg-Fq7cmAvdjnsV96zE .icon-shape,#mermaid-svg-Fq7cmAvdjnsV96zE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fq7cmAvdjnsV96zE .icon-shape p,#mermaid-svg-Fq7cmAvdjnsV96zE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Fq7cmAvdjnsV96zE .icon-shape .label rect,#mermaid-svg-Fq7cmAvdjnsV96zE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fq7cmAvdjnsV96zE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Fq7cmAvdjnsV96zE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Fq7cmAvdjnsV96zE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 导入 spring-boot-starter-cache
扩展 CacheManager 实现类
EhCache/Caffeine/Guava...
导入 spring-boot-starter-data-redis
引入 RedisCacheManager
CacheAutoConfiguration 按优先级选择
RedisCacheManager 生效
缓存数据存入 Redis
key = 缓存名::SpEL结果
一、为什么要切换到 Redis
前面几篇的入门案例都跑在 ConcurrentMapCacheManager 上,四个致命缺陷决定了它只能用于演示:
| 缺陷 | 具体表现 |
|---|---|
| 重启丢失 | 每次重启服务,缓存全部清空,第一批请求全部打到数据库 |
| 无法共享 | 多实例部署时每个 JVM 一份缓存,实例间数据不一致 |
| 无过期时间 | 数据永不过期,只能靠代码主动清理,漏清就是永久脏数据 |
| 无容量限制 | 缓存无上限增长,最终 OutOfMemoryError |
Redis 四点全部解决:数据独立存储在 Redis 进程(重启应用不丢失)、所有实例共享同一份、原生支持 TTL、可通过 maxmemory 配置容量上限与淘汰策略。
二、导入依赖
需要两个坐标,缺一不可:
xml
<!-- 缓存抽象:提供 CacheAutoConfiguration、CacheProperties,以及 EhCache/Caffeine 等扩展 CacheManager -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 支持:提供 RedisCacheManager -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
各自的职责
#mermaid-svg-mmacJ5rEKOZMWUTe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mmacJ5rEKOZMWUTe .error-icon{fill:#552222;}#mermaid-svg-mmacJ5rEKOZMWUTe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mmacJ5rEKOZMWUTe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mmacJ5rEKOZMWUTe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mmacJ5rEKOZMWUTe .marker.cross{stroke:#333333;}#mermaid-svg-mmacJ5rEKOZMWUTe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mmacJ5rEKOZMWUTe p{margin:0;}#mermaid-svg-mmacJ5rEKOZMWUTe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe .cluster-label text{fill:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe .cluster-label span{color:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe .cluster-label span p{background-color:transparent;}#mermaid-svg-mmacJ5rEKOZMWUTe .label text,#mermaid-svg-mmacJ5rEKOZMWUTe span{fill:#333;color:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe .node rect,#mermaid-svg-mmacJ5rEKOZMWUTe .node circle,#mermaid-svg-mmacJ5rEKOZMWUTe .node ellipse,#mermaid-svg-mmacJ5rEKOZMWUTe .node polygon,#mermaid-svg-mmacJ5rEKOZMWUTe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mmacJ5rEKOZMWUTe .rough-node .label text,#mermaid-svg-mmacJ5rEKOZMWUTe .node .label text,#mermaid-svg-mmacJ5rEKOZMWUTe .image-shape .label,#mermaid-svg-mmacJ5rEKOZMWUTe .icon-shape .label{text-anchor:middle;}#mermaid-svg-mmacJ5rEKOZMWUTe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mmacJ5rEKOZMWUTe .rough-node .label,#mermaid-svg-mmacJ5rEKOZMWUTe .node .label,#mermaid-svg-mmacJ5rEKOZMWUTe .image-shape .label,#mermaid-svg-mmacJ5rEKOZMWUTe .icon-shape .label{text-align:center;}#mermaid-svg-mmacJ5rEKOZMWUTe .node.clickable{cursor:pointer;}#mermaid-svg-mmacJ5rEKOZMWUTe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mmacJ5rEKOZMWUTe .arrowheadPath{fill:#333333;}#mermaid-svg-mmacJ5rEKOZMWUTe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mmacJ5rEKOZMWUTe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mmacJ5rEKOZMWUTe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mmacJ5rEKOZMWUTe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mmacJ5rEKOZMWUTe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mmacJ5rEKOZMWUTe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mmacJ5rEKOZMWUTe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mmacJ5rEKOZMWUTe .cluster text{fill:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe .cluster span{color:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mmacJ5rEKOZMWUTe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mmacJ5rEKOZMWUTe rect.text{fill:none;stroke-width:0;}#mermaid-svg-mmacJ5rEKOZMWUTe .icon-shape,#mermaid-svg-mmacJ5rEKOZMWUTe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mmacJ5rEKOZMWUTe .icon-shape p,#mermaid-svg-mmacJ5rEKOZMWUTe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mmacJ5rEKOZMWUTe .icon-shape .label rect,#mermaid-svg-mmacJ5rEKOZMWUTe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mmacJ5rEKOZMWUTe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mmacJ5rEKOZMWUTe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mmacJ5rEKOZMWUTe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} spring-context
(starter-web 传递依赖)
Cache / CacheManager 接口
ConcurrentMapCacheManager
NoOpCacheManager
@Cacheable 等注解
spring-boot-starter-cache
CacheAutoConfiguration
CacheProperties
绑定 spring.cache.*
EhCacheCacheManager
CaffeineCacheManager
spring-boot-starter-data-redis
RedisCacheManager
RedisCacheConfiguration
RedisTemplate / Lettuce
课程里用了一个很直观的验证方法:在 IDEA 中查看 CacheManager 接口的实现类数量。
| 阶段 | CacheManager 实现类 |
|---|---|
只有 starter-web |
ConcurrentMapCacheManager、NoOpCacheManager 等少数几个(都在 spring-context 包) |
加入 starter-cache |
增加 EhCacheCacheManager、CaffeineCacheManager、GuavaCacheManager、JCacheCacheManager 等 |
再加入 starter-data-redis |
增加 RedisCacheManager(在 spring-data-redis 包) |
这个技巧值得记住:想确认引入了哪个
CacheManager,直接看接口的实现类列表 ,比翻文档快。在IDEA里点击接口名左侧的实现类图标,或按Ctrl+Alt+B。
三、配置 application.yml
yaml
spring:
redis:
host: 172.17.2.94
port: 6379
password: root@123456
database: 0
cache:
redis:
time-to-live: 1800000 # 缓存过期时间 30 分钟(毫秒)
| 配置项 | 说明 |
|---|---|
spring.redis.host |
Redis 服务地址 |
spring.redis.port |
默认 6379 |
spring.redis.password |
未启用 requirepass 时可省略 |
spring.redis.database |
默认 0 |
spring.cache.redis.time-to-live |
全局默认过期时间(毫秒),可省略 |
关于 time-to-live:
- 单位毫秒 ,
1800000毫秒 =1800秒 =30分钟 - 不配置则永不过期 。这与手工实现时"必须设
TTL"的结论一致,只是现在由框架统一兜底 - 它是全局默认值 。若要为某个缓存分区单独设置,需要自定义
RedisCacheManager(见后文)
其他可选配置:
yaml
spring:
cache:
type: redis # 强制指定缓存类型(自动推断失效时可用)
redis:
time-to-live: 1800000
cache-null-values: true # 是否缓存 null 值,默认 true
key-prefix: "reggie:" # 全局 key 前缀
use-key-prefix: true # 是否使用前缀,默认 true
四、实测:condition 的坑真实发生了
课程里切换完 Redis 后做第一次测试,发现"缓存没生效"------第二次请求依然进入了方法体。这正是前一篇预警过的问题,这里完整还原。
问题代码
java
// 错误写法
@Cacheable(value = "userCache", key = "#id", condition = "#result != null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userService.getById(id);
}
现象
Redis 方法体 CacheInterceptor Postman Redis 方法体 CacheInterceptor Postman #mermaid-svg-Hqber2xTgSzAV6Ta{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Hqber2xTgSzAV6Ta .error-icon{fill:#552222;}#mermaid-svg-Hqber2xTgSzAV6Ta .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Hqber2xTgSzAV6Ta .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Hqber2xTgSzAV6Ta .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Hqber2xTgSzAV6Ta .marker.cross{stroke:#333333;}#mermaid-svg-Hqber2xTgSzAV6Ta svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Hqber2xTgSzAV6Ta p{margin:0;}#mermaid-svg-Hqber2xTgSzAV6Ta .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Hqber2xTgSzAV6Ta text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Hqber2xTgSzAV6Ta .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Hqber2xTgSzAV6Ta .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Hqber2xTgSzAV6Ta #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Hqber2xTgSzAV6Ta .sequenceNumber{fill:white;}#mermaid-svg-Hqber2xTgSzAV6Ta #sequencenumber{fill:#333;}#mermaid-svg-Hqber2xTgSzAV6Ta #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Hqber2xTgSzAV6Ta .messageText{fill:#333;stroke:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Hqber2xTgSzAV6Ta .labelText,#mermaid-svg-Hqber2xTgSzAV6Ta .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .loopText,#mermaid-svg-Hqber2xTgSzAV6Ta .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Hqber2xTgSzAV6Ta .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Hqber2xTgSzAV6Ta .noteText,#mermaid-svg-Hqber2xTgSzAV6Ta .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Hqber2xTgSzAV6Ta .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Hqber2xTgSzAV6Ta .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Hqber2xTgSzAV6Ta .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Hqber2xTgSzAV6Ta .actorPopupMenu{position:absolute;}#mermaid-svg-Hqber2xTgSzAV6Ta .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Hqber2xTgSzAV6Ta .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Hqber2xTgSzAV6Ta .actor-man circle,#mermaid-svg-Hqber2xTgSzAV6Ta line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Hqber2xTgSzAV6Ta :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 此时方法未执行 执行后不再评估 condition结果不写入缓存 缓存中无数据,再次执行方法 GET /user/1001(首次)评估 condition:跳过缓存查询,直接执行方法返回 User返回 UserGET /user/1001(第二次)执行方法返回 User
结果:每次都查数据库,缓存形同虚设,且无任何报错。
根因
查看 @Cacheable 注解的源码注释,condition 可用的上下文对象只有:
| 属性 | 可用上下文 |
|---|---|
condition |
#root、方法参数(#id、#p0、#root.args[0]) |
unless |
#root、方法参数、#result |
condition 在方法执行前 就要决定是否走缓存逻辑,那时根本没有 #result。SpEL 对不存在的变量求值得到 null,"#result != null" 恒为 false。
修正
java
// 正确写法
@Cacheable(value = "userCache", key = "#id", unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userService.getById(id);
}
修正后重测:
| 请求 | 行为 |
|---|---|
GET /user/1001(首次) |
进入方法体,Redis 出现 userCache::1001 |
GET /user/1001(第二次、第三次) |
不进入方法体,直接返回缓存 |
GET /user/12345(不存在的 ID) |
进入方法体,返回 null,不写入缓存 |
GET /user/12345(再次) |
仍进入方法体 ------ 证明 unless 生效 |
这条判据值得背下来:
@Cacheable里判断返回值,只能用unless,不能用condition。
五、Redis 中的 key 结构
切换后,缓存数据在 Redis 中长这样:
userCache::1502198504921124865
结构拆解:
#mermaid-svg-RykVFly7eLPHLcS8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RykVFly7eLPHLcS8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RykVFly7eLPHLcS8 .error-icon{fill:#552222;}#mermaid-svg-RykVFly7eLPHLcS8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RykVFly7eLPHLcS8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RykVFly7eLPHLcS8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RykVFly7eLPHLcS8 .marker.cross{stroke:#333333;}#mermaid-svg-RykVFly7eLPHLcS8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RykVFly7eLPHLcS8 p{margin:0;}#mermaid-svg-RykVFly7eLPHLcS8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RykVFly7eLPHLcS8 .cluster-label text{fill:#333;}#mermaid-svg-RykVFly7eLPHLcS8 .cluster-label span{color:#333;}#mermaid-svg-RykVFly7eLPHLcS8 .cluster-label span p{background-color:transparent;}#mermaid-svg-RykVFly7eLPHLcS8 .label text,#mermaid-svg-RykVFly7eLPHLcS8 span{fill:#333;color:#333;}#mermaid-svg-RykVFly7eLPHLcS8 .node rect,#mermaid-svg-RykVFly7eLPHLcS8 .node circle,#mermaid-svg-RykVFly7eLPHLcS8 .node ellipse,#mermaid-svg-RykVFly7eLPHLcS8 .node polygon,#mermaid-svg-RykVFly7eLPHLcS8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RykVFly7eLPHLcS8 .rough-node .label text,#mermaid-svg-RykVFly7eLPHLcS8 .node .label text,#mermaid-svg-RykVFly7eLPHLcS8 .image-shape .label,#mermaid-svg-RykVFly7eLPHLcS8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-RykVFly7eLPHLcS8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RykVFly7eLPHLcS8 .rough-node .label,#mermaid-svg-RykVFly7eLPHLcS8 .node .label,#mermaid-svg-RykVFly7eLPHLcS8 .image-shape .label,#mermaid-svg-RykVFly7eLPHLcS8 .icon-shape .label{text-align:center;}#mermaid-svg-RykVFly7eLPHLcS8 .node.clickable{cursor:pointer;}#mermaid-svg-RykVFly7eLPHLcS8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RykVFly7eLPHLcS8 .arrowheadPath{fill:#333333;}#mermaid-svg-RykVFly7eLPHLcS8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RykVFly7eLPHLcS8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RykVFly7eLPHLcS8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RykVFly7eLPHLcS8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RykVFly7eLPHLcS8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RykVFly7eLPHLcS8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RykVFly7eLPHLcS8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RykVFly7eLPHLcS8 .cluster text{fill:#333;}#mermaid-svg-RykVFly7eLPHLcS8 .cluster span{color:#333;}#mermaid-svg-RykVFly7eLPHLcS8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RykVFly7eLPHLcS8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RykVFly7eLPHLcS8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-RykVFly7eLPHLcS8 .icon-shape,#mermaid-svg-RykVFly7eLPHLcS8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RykVFly7eLPHLcS8 .icon-shape p,#mermaid-svg-RykVFly7eLPHLcS8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RykVFly7eLPHLcS8 .icon-shape .label rect,#mermaid-svg-RykVFly7eLPHLcS8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RykVFly7eLPHLcS8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RykVFly7eLPHLcS8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RykVFly7eLPHLcS8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} userCache
完整 key
::
1502198504921124865
value: User 对象的序列化结果
| 部分 | 来源 | 说明 |
|---|---|---|
userCache |
注解的 value / cacheNames |
缓存分区名,在 Redis 中作为 key 前缀 |
:: |
RedisCacheConfiguration 默认分隔符 |
可通过 computePrefixWith() 自定义 |
1502198504921124865 |
SpEL 计算结果 |
本例是 #id |
用 redis-cli 观察:
bash
127.0.0.1:6379> KEYS *
1) "userCache::1502198504921124865"
127.0.0.1:6379> TTL userCache::1502198504921124865
(integer) 1789
127.0.0.1:6379> TYPE userCache::1502198504921124865
string
TTL 接近 1800 秒,说明 time-to-live 配置生效了。
注意 :
Spring Cache的RedisCacheManager与手工注入的RedisTemplate是两套独立的序列化配置 。RedisConfig里给RedisTemplate换的StringRedisSerializer不会影响RedisCacheManager。后者的默认序列化器是JdkSerializationRedisSerializer(value)与StringRedisSerializer(key),所以value依然显示为二进制。要改需要自定义RedisCacheManager(见后文)。
六、验证 @CacheEvict 在 Redis 下同样生效
执行 DELETE /user/{id}:
Redis MySQL delete 方法 CacheInterceptor Postman Redis MySQL delete 方法 CacheInterceptor Postman #mermaid-svg-aarTi5pOJXSeUlRz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aarTi5pOJXSeUlRz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aarTi5pOJXSeUlRz .error-icon{fill:#552222;}#mermaid-svg-aarTi5pOJXSeUlRz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aarTi5pOJXSeUlRz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aarTi5pOJXSeUlRz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aarTi5pOJXSeUlRz .marker.cross{stroke:#333333;}#mermaid-svg-aarTi5pOJXSeUlRz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aarTi5pOJXSeUlRz p{margin:0;}#mermaid-svg-aarTi5pOJXSeUlRz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aarTi5pOJXSeUlRz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aarTi5pOJXSeUlRz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aarTi5pOJXSeUlRz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aarTi5pOJXSeUlRz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aarTi5pOJXSeUlRz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aarTi5pOJXSeUlRz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aarTi5pOJXSeUlRz .sequenceNumber{fill:white;}#mermaid-svg-aarTi5pOJXSeUlRz #sequencenumber{fill:#333;}#mermaid-svg-aarTi5pOJXSeUlRz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aarTi5pOJXSeUlRz .messageText{fill:#333;stroke:none;}#mermaid-svg-aarTi5pOJXSeUlRz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aarTi5pOJXSeUlRz .labelText,#mermaid-svg-aarTi5pOJXSeUlRz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aarTi5pOJXSeUlRz .loopText,#mermaid-svg-aarTi5pOJXSeUlRz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aarTi5pOJXSeUlRz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aarTi5pOJXSeUlRz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aarTi5pOJXSeUlRz .noteText,#mermaid-svg-aarTi5pOJXSeUlRz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aarTi5pOJXSeUlRz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aarTi5pOJXSeUlRz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aarTi5pOJXSeUlRz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aarTi5pOJXSeUlRz .actorPopupMenu{position:absolute;}#mermaid-svg-aarTi5pOJXSeUlRz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aarTi5pOJXSeUlRz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aarTi5pOJXSeUlRz .actor-man circle,#mermaid-svg-aarTi5pOJXSeUlRz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aarTi5pOJXSeUlRz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DELETE /user/1001执行方法体DELETE FROM user WHERE id=1001成功解析DEL userCache::1001OK200 OK
在 Redis 客户端刷新,该 key 已消失。数据库记录也被删除。
注解的用法与底层缓存产品无关 ------这正是抽象层的价值。从 ConcurrentMap 切到 Redis,业务代码一行没改。
七、自定义 RedisCacheManager
默认的 RedisCacheManager 有两个不便:value 是 JDK 二进制、所有分区共用同一个 TTL。通过自定义 CacheManager 可以解决:
java
package com.itheima.reggie.config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义 RedisCacheManager:JSON 序列化 + 按分区设置不同过期时间
*/
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
/** 菜品缓存:60 分钟 */
private static final Duration DISH_TTL = Duration.ofMinutes(60);
/** 套餐缓存:60 分钟 */
private static final Duration SETMEAL_TTL = Duration.ofMinutes(60);
/** 默认:30 分钟 */
private static final Duration DEFAULT_TTL = Duration.ofMinutes(30);
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// key 用 String,value 用 JSON
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// 不缓存 null 值(全局生效,注解里的 unless 仍可覆盖)
.disableCachingNullValues()
.entryTtl(DEFAULT_TTL);
// 为不同分区指定不同过期时间
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("dishCache", config.entryTtl(DISH_TTL));
cacheConfigs.put("setmealCache", config.entryTtl(SETMEAL_TTL));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
// 启用事务支持(可选)
.transactionAware()
.build();
}
}
配置项说明:
| 方法 | 作用 |
|---|---|
serializeKeysWith |
设置 key 序列化器 |
serializeValuesWith |
设置 value 序列化器 |
entryTtl(Duration) |
设置该分区的过期时间 |
disableCachingNullValues() |
全局禁用 null 值缓存 |
prefixCacheNameWith(String) |
自定义 key 前缀 |
computePrefixWith(CacheKeyPrefix) |
自定义前缀计算逻辑 |
withInitialCacheConfigurations(Map) |
按分区指定不同配置 |
transactionAware() |
缓存操作与 Spring 事务同步(事务提交后才写缓存) |
注意 :一旦自定义了
RedisCacheManager这个Bean,application.yml中的spring.cache.redis.time-to-live就不再生效 了------因为自动配置的CacheManager上有@ConditionalOnMissingBean(CacheManager.class),用户定义的会覆盖它。过期时间要写在代码里。
序列化器选择的影响 :GenericJackson2JsonRedisSerializer会在JSON里写入@class字段记录类型,反序列化时能还原成User/DishDto具体类型。如果换成Jackson2JsonRedisSerializer<Object>而不指定类型,反序列化出来会是LinkedHashMap,强转时抛ClassCastException。
八、项目目录结构
txt
cache_demo/
├── pom.xml # 【本轮】新增 starter-cache + starter-data-redis
└── src/main/
├── java/com/itheima/
│ ├── CacheDemoApplication.java # @EnableCaching
│ ├── config/
│ │ └── RedisCacheConfig.java # 【可选】自定义 CacheManager
│ ├── controller/
│ │ └── UserController.java # 【本轮】condition 改为 unless
│ ├── entity/
│ │ └── User.java
│ ├── mapper/
│ │ └── UserMapper.java
│ └── service/
│ ├── UserService.java
│ └── impl/UserServiceImpl.java
└── resources/
└── application.yml # 【本轮】新增 spring.redis.* 与 spring.cache.redis.*
九、完整可运行代码
pom.xml(关键依赖)
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cache 自动配置与 CacheProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 支持,提供 RedisCacheManager -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
</dependencies>
application.yml
yaml
server:
port: 8080
spring:
application:
name: cache_demo
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cache_demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
# Redis 连接信息
redis:
host: 172.17.2.94
port: 6379
password: root@123456
database: 0
# 缓存过期时间(毫秒),1800000ms = 30 分钟
cache:
type: redis
redis:
time-to-live: 1800000
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
UserController(修正 unless)
java
package com.itheima.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.entity.User;
import com.itheima.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* 用户控制器:底层缓存产品为 Redis
*
* <p>注解用法与 ConcurrentMapCacheManager 完全一致,
* 切换缓存产品不需要修改业务代码。</p>
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private CacheManager cacheManager;
@Autowired
private UserService userService;
/**
* CachePut:新增用户,返回值写入缓存
*/
@CachePut(value = "userCache", key = "#result.id")
@PostMapping
public User save(User user) {
userService.save(user);
return user;
}
/**
* CacheEvict:删除用户,清理对应缓存
*/
@CacheEvict(value = "userCache", key = "#id")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
userService.removeById(id);
}
/**
* CacheEvict:更新用户,清理对应缓存
*/
@CacheEvict(value = "userCache", key = "#user.id")
@PutMapping
public User update(User user) {
userService.updateById(user);
return user;
}
/**
* Cacheable:根据 id 查询用户
*
* <p>此处必须用 unless 而非 condition:
* condition 在方法执行前评估,取不到 #result,
* 会导致条件恒为 false、缓存永不命中。</p>
*/
@Cacheable(value = "userCache", key = "#id", unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
User user = userService.getById(id);
return user;
}
/**
* Cacheable:条件查询列表,key 由 id 与 name 共同组成
*/
@Cacheable(value = "userCache", key = "#user.id + '_' + #user.name")
@GetMapping("/list")
public List<User> list(User user) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(user.getId() != null, User::getId, user.getId());
queryWrapper.eq(user.getName() != null, User::getName, user.getName());
List<User> list = userService.list(queryWrapper);
return list;
}
/**
* 调试接口:查看当前生效的 CacheManager 类型
*/
@GetMapping("/cacheManagerType")
public String cacheManagerType() {
return cacheManager.getClass().getName();
}
/**
* 调试接口:所有缓存分区名称
*/
@GetMapping("/cacheNames")
public List<String> cacheNames() {
return new ArrayList<>(cacheManager.getCacheNames());
}
}
验证步骤与 curl
bash
# 确认 CacheManager 类型(应输出 ...RedisCacheManager)
curl "http://localhost:8080/user/cacheManagerType"
# 首次查询:进入方法体,Redis 出现 userCache::<id>
curl "http://localhost:8080/user/1502198504921124865"
# 再次查询:不进入方法体(缓存命中)
curl "http://localhost:8080/user/1502198504921124865"
# 查询不存在的 ID:返回 null,且不写入缓存
curl "http://localhost:8080/user/12345"
# 删除:数据库记录与 Redis key 同时消失
curl -X DELETE "http://localhost:8080/user/1502198504921124865"
Redis 侧观察:
bash
127.0.0.1:6379> KEYS *
1) "userCache::1502198504921124865"
127.0.0.1:6379> TTL userCache::1502198504921124865
(integer) 1795
# 查询不存在的 ID 后,不应出现 userCache::12345
127.0.0.1:6379> KEYS *
1) "userCache::1502198504921124865"
# 执行 DELETE 后
127.0.0.1:6379> KEYS *
(empty array)
API 速览
| API / 配置 | 所在包 / 文件 | 作用 |
|---|---|---|
spring-boot-starter-cache |
Maven 坐标 | 缓存自动配置与 CacheProperties |
spring-boot-starter-data-redis |
Maven 坐标 | 提供 RedisCacheManager |
spring.cache.type |
application.yml |
强制指定缓存类型 |
spring.cache.redis.time-to-live |
application.yml |
全局缓存过期时间(毫秒) |
spring.cache.redis.cache-null-values |
application.yml |
是否缓存 null,默认 true |
spring.cache.redis.key-prefix |
application.yml |
全局 key 前缀 |
RedisCacheManager |
org.springframework.data.redis.cache |
Redis 缓存管理器 |
RedisCacheConfiguration |
org.springframework.data.redis.cache |
Redis 缓存配置类 |
RedisCacheManager.builder() |
RedisCacheManager |
构建器入口 |
cacheDefaults(RedisCacheConfiguration) |
RedisCacheManager.RedisCacheManagerBuilder |
设置默认配置 |
withInitialCacheConfigurations(Map) |
Builder | 按分区设置不同配置 |
entryTtl(Duration) |
RedisCacheConfiguration |
设置过期时间 |
serializeKeysWith(SerializationPair) |
RedisCacheConfiguration |
设置 key 序列化器 |
serializeValuesWith(SerializationPair) |
RedisCacheConfiguration |
设置 value 序列化器 |
disableCachingNullValues() |
RedisCacheConfiguration |
禁用 null 值缓存 |
prefixCacheNameWith(String) |
RedisCacheConfiguration |
自定义 key 前缀 |
transactionAware() |
Builder | 缓存操作与事务同步 |
GenericJackson2JsonRedisSerializer |
org.springframework.data.redis.serializer |
带类型信息的 JSON 序列化器 |
官方文档
总结
切换缓存产品只需要两步:导入两个坐标 + 配置 Redis 连接 。业务代码一行不用改------这就是 Spring Cache 抽象层的实际价值。
spring-boot-starter-cache 提供自动配置与 CacheProperties,spring-boot-starter-data-redis 提供 RedisCacheManager,两者缺一不可。判断当前用的是哪个 CacheManager,最快的方式是在 IDE 里看 CacheManager 接口的实现类列表,或者直接打印 cacheManager.getClass().getName()。
本篇最有价值的部分是实测复现了 condition 的坑 :切换 Redis 后第一次测试就发现缓存没生效,根因是 condition = "#result != null" 在方法执行前评估,#result 恒为 null,条件永远不成立。改成 unless = "#result == null" 后恢复正常。这印证了前一篇的结论------判断返回值只能用 unless。
Redis 中 key 的格式是 缓存名::SpEL结果,如 userCache::1502198504921124865。另外要特别注意:RedisTemplate 的序列化器配置与 RedisCacheManager 的是两套独立的配置 ,前者改了不影响后者,想让缓存 value 也变成可读 JSON,必须自定义 RedisCacheManager(文中给出了完整代码,含按分区设置不同 TTL)。
最后提醒一个连带效应:自定义 CacheManager 后,application.yml 里的 spring.cache.redis.time-to-live 会失效(自动配置被 @ConditionalOnMissingBean 覆盖),过期时间要写进代码。
下一篇回到外卖平台项目,用 Spring Cache 改造套餐数据的缓存。