Java 项目实战: 外卖平台优化-使用Redis作为缓存产品与TTL配置

缓存优化: 切换 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 序列化器

官方文档

主题 链接
Spring Boot 缓存 Redis 配置 https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/#boot-features-caching-provider-redis
RedisCacheManager JavaDoc https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/cache/RedisCacheManager.html
RedisCacheConfiguration JavaDoc https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/cache/RedisCacheConfiguration.html
CacheProperties 配置项 https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/#appendix.application-properties.cache

总结

切换缓存产品只需要两步:导入两个坐标 + 配置 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 改造套餐数据的缓存。

相关推荐
谢亮_vipxieliang1 小时前
ValidX 在微服务架构中的验证策略
java·spring boot·spring·spring cloud·hibernate
其实防守也摸鱼1 小时前
内网安全防护实践:从攻击视角理解防御要点
java·前端·安全·架构·自动化·nps
我是小白呀1 小时前
17-企业Workflow引擎怎么选:定时、队列、n8n、Temporal与Camunda
java·开发语言·人工智能·dubbo·workflow
海宇AI1 小时前
零信任架构实战:基于海宇车型识别精准构建自动化车队评估微服务
java·人工智能·架构·自动化
xixiaoyunya1 小时前
Spring Boot 3 统一响应与全局异常处理实战
java·spring boot·后端
一条破秋裤2 小时前
Linux 线程分离与主动取消:pthread_detach、pthread_cancel
java·linux·jvm
茉莉玫瑰花茶2 小时前
OpenGL [ 基础概念 ]
java·前端·数据库
周杰偷奶茶2 小时前
【Java】运算符指南
java·开发语言