资讯动态

Spring Cache五大注解详解:从入门到缓存实战避坑指南

发布时间:2026/10/20 7:22:54 来源:尧图企业网站定制
做后端开发的几乎都见过这种场景QPS 稍微上来一点数据库连接先报警然后代码里开始到处塞缓存逻辑。读的时候先查 Redis 再查库写的时候先更库再删缓存每个方法前面都要手写一坨缓存模板代码业务逻辑还没写几句缓存代码倒是占了大半屏而且换个缓存中间件还得全局搜索替换。Spring 缓存注解就是用来解决这个痛点的基于 Spring Cache 抽象层Cacheable、CachePut、CacheEvict、Caching、CacheConfig这五个核心注解基本覆盖了日常开发里九成以上的缓存场景。这篇文章会把每个注解的常用属性拆开讲透比如 key 怎么用 SpEL 生成、condition 和 unless 有什么区别、allEntries 和 beforeInvocation 会在什么场景下坑你一次再配合一个基于 Redis 的完整可运行示例。适合两类人看刚接触 Spring Cache 想快点上手的新人以及用了一两年注解但总是在缓存失效、key 冲突、序列化乱码这些暗坑里翻车的老同学。1. 为什么 Spring 缓存注解能帮你少写一半缓存代码1.1 手写缓存代码到底疼在哪里先回想一下没有 Spring Cache 之前我们是怎么写缓存的。一个查询用户的方法逻辑往往长得差不多public User getUserById(Long id) { String key user: id; // 1.查缓存 Object cached redisTemplate.opsForValue().get(key); if (cached ! null) { return (User) cached; } // 2.缓存没有查数据库 User user userMapper.selectById(id); // 3.回填缓存 if (user ! null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES); } return user; }这个写法的问题不是“能不能跑”而是它把缓存行为和业务查询逻辑强行耦合在了一个方法里。每多一个查询接口就要复制一遍这段模板缓存中间件从 Redis 换成 Caffeine所有方法都要动key 的生成规则散落在各个方法里时间长了根本没有办法统一管理。本地缓存、分布式缓存、多级缓存这些更复杂的策略更是想都不要想。说白了这种代码在前期能顶一阵子一旦项目进入维护期“查缓存、回填缓存”这部分代码就成了纯粹的噪音。Spring Cache 做的事情就是把“缓存读写”这个横切逻辑从业务方法里抽出来。业务方法只管查数据库、返回结果至于结果要不要缓存、缓存 key 是什么、过期时间多久全部通过注解声明。框架在方法调用前后自动完成缓存的读取、写入和清理开发者不需要关心缓存的底层实现到底是 ConcurrentMap、Redis 还是 Caffeine。1.2 理解缓存抽象的三个角色要会用注解建议先花两分钟理解 Spring Cache 抽象层里的三个角色概念通了很多属性配置就是水到渠成的事情。第一个角色是CacheManager负责缓存区域的管理者。一个应用里可以有多个 CacheManager比如一个本地 Caffeine 一个 Redis也可以只有一个。它的职责是创建并管理所有Cache实例。第二个角色是Cache代表一个具体命名的缓存区域。这个概念容易理解成“一个缓存”其实更准确地说它是“一类缓存”。比如我们把用户信息放user:detail把用户列表放user:list这是两个不同的 Cache 区域。每个 Cache 区域内部才是具体的 key-value。为什么要区分区域因为不同业务的 key 可能同名分开存放能天然隔离也方便单独设置过期时间、单独清空。第三个角色就是注解本身。Cacheable、CachePut、CacheEvict这些注解是开发者跟缓存打交道的主要入口。Spring 通过 AOP 拦截被注解标注的方法在方法前后织入缓存逻辑。理解到这一层后面的属性配置就只是“查手册”了。一个最简单的缓存用法只需要三步在启动类加EnableCaching开启缓存能力在方法上加Cacheable(cacheNames user, key #id)然后配置一个可用的 CacheManager比如 Spring Boot 默认的ConcurrentMapCacheManager或者集成 Redis。一个查询方法就具备“先查缓存、未命中再查库、查完回填缓存”的能力了。2. 五大核心注解属性拆解每个属性都有示例2.1 Cacheable查询缓存的主力六个属性一次说透Cacheable是使用频率最高的缓存注解它标注在查询方法上表示“方法结果需要缓存”。请求进来时Spring 先根据缓存区域和 key 去缓存里查命中就直接返回结果不再执行方法体未命中才执行方法并把返回值写进缓存。先看一个完整示例再逐个属性拆解Cacheable( cacheNames user:detail, key #id, condition #id 0, unless #result null, sync true ) public User getUserById(Long id) { return userMapper.selectById(id); }第一个属性是cacheNames也可以写成value两者等价。它指定结果放到哪个缓存区域支持数组也就是一个方法的结果可以同时写入多个缓存区域。这个属性是必填的不填启动不会报错但运行时无法确定放进哪个 Cache 区域等于没写。建议在项目里统一使用带业务前缀的名字比如user:detail、order:list可读性比user、order好很多排查问题的时候能省不少时间。第二个属性是key默认情况下 Spring 使用 SimpleKeyGenerator把方法的所有参数拼成一个 key。如果方法只有一个参数key 就是这个参数本身如果有多个参数key 就是参数组合。绝大多数场景我们不满足于默认比如getUserById(Long id)没问题但getUserByName(String name, String tenant)这种多参数方法默认把所有参数都拼进去可能会导致缓存粒度过细或者 key 重复。用key #tenant : #name就能自定义成业务期望的形式。key 属性支持 Spring 表达式语言SpEL有几个内置对象和变量非常常用#root.methodName取方法名#root.targetClass取目标类#参数名取方法入参#result取方法返回值。比如key #root.methodName : #id就是把方法名和参数拼在一起能避免两个不同方法使用相同缓存区域时 key 冲突。不过更推荐的做法是直接在缓存区域上区分业务区域不同基本不会冲突。第三个属性是keyGenerator这是一个自定义 key 生成器。它和key是互斥的两者同时指定会报错。什么时候需要自定义当项目里 key 的生成规则非常统一比如都要拼接系统标识、租户 ID每个方法都写 SpEL 容易遗漏且难维护。实现KeyGenerator接口重写generate方法然后在注解里指定keyGenerator customKeyGenerator即可。注意这里的值不是类名而是 Bean 名称。第四个属性是condition方法调用前评估的布尔条件。condition 为 true 才执行方法的缓存逻辑为 false 则方法正常执行但结果不进缓存。例如condition #id 0id 小于等于 0 的请求直接放过不查缓存也不写缓存。这个属性适合给参数加前置校验比如有些查询参数是空字符串或默认值完全没有缓存意义。第五个属性是unless和 condition 看起来像实际触发时机不同。unless 是在方法调用完成后根据返回值#result来评估的。unless #result null的意思是“当返回结果为空时不缓存”这在逻辑上很自然。但要注意这意味着 null 结果每次都穿透到数据库。如果你的接口容易被恶意刷null 也是一种攻击放大信号后面第四章会详细说空值缓存的问题。第六个属性是sync这是解决缓存击穿的一个开关。默认 false多个线程同时请求一个未命中的 key都会去执行方法查库。开启sync true后同一个 key 的并发请求只有一个线程会真正执行方法其他线程等待结果。代价是 sync 模式下不能与 unless 一起使用因为 unless 要执行完方法才能评估而 sync 的语义是提前锁住 key 避免并发重复查询两个逻辑有冲突Spring 会直接报错。Cacheable还有一个冷门但实用的属性cacheManager当应用里配置了多个 CacheManager 时用它指定当前方法走哪个缓存管理器。比如本地缓存和 Redis 缓存混合的场景不同方法可以选择不同缓存介质。2.2 CachePut只写缓存不读缓存更新数据时同步刷新CachePut和Cacheable长得一样但语义完全相反。它标注的方法每次都会执行执行完把返回值写入缓存。它存在的意义是解决“数据更新后缓存怎么刷新”的问题。打个比方Cacheable是“先看冰箱里有没有菜没有就去买菜并放进去”CachePut是“直接买菜放进冰箱下一次再看冰箱”。所以CachePut模式的接口每次调用都会触碰数据库适合新增、更新这类操作执行后同步刷缓存。CachePut(cacheNames user:detail, key #user.id) public User updateUser(User user) { userMapper.updateById(user); return user; }这里有一个很容易忽略的坑CachePut写入缓存的是方法的返回值。如果更新方法返回值是 void 或者不是完整对象缓存里存的就可能是 null 或残缺对象下次查询直接翻车。我在项目里见过有人把 update 方法写成public void updateUser(User user)然后在方法里调用了 cacheManager 显式写缓存才避开问题。在 Spring Cache 的声明式编程里返回值的类型和完整性直接决定缓存质量这一点务必注意。CachePut支持的属性和Cacheable几乎一致包括 cacheNames、key、condition、unless也支持 SpEL。不过 sync 对它没有意义官方文档也没有要求。同类的key #user.id写法要保证更新操作传入的 user 对象里主键不为空否则会把一个没意义的 key 写进缓存。2.3 CacheEvict删除缓存也有讲究有写入就有清理CacheEvict就是干这个的。它标注在方法上方法执行后或执行前删除指定缓存区域中的指定 key或者清空整个缓存区域。CacheEvict(cacheNames user:detail, key #id) public void deleteUser(Long id) { userMapper.deleteById(id); }基础用法很简单但有两个属性非常关键理解不深很容易出生产事故。第一个是allEntries默认 false表示只删除 key 对应的缓存条目设为 true 表示清空该缓存区域的所有缓存。什么时候用 true当缓存区域里的 key 无法穷举或者数据批量变更导致大量 key 失效时。比如用户列表缓存删除一个用户会影响列表数据但列表缓存的 key 可能是page1size10这种组合没法精确定位到某个 key干脆allEntries true把整个区域清掉。代价就是清空瞬间所有请求都会打到数据库要做好这个心理准备。第二个是beforeInvocation默认 false表示方法成功执行完之后再删缓存设为 true 表示方法执行之前就删缓存。默认行为有个隐患如果方法抛异常缓存就不会被删除此时数据库没变、缓存还是旧数据短期内会读到脏数据。那是不是设成 true 就万事大吉了也不是。beforeInvocationtrue 时即使方法后续抛异常缓存也已经删掉了下一次查询会重新加载数据效果等于是“宁可多查一次库也不留脏缓存”。对于删除操作我个人的习惯是删除方法返回值不是关键时用默认的 false 即可因为如果数据库都删失败了旧缓存留着反而能兜底虽然也是脏数据但删除缓存失败造成下次查库才是更大的损耗。更稳妥的做法是配合事务同步这一点在第四章展开。CacheEvict同样支持多个缓存区域数组清理比如cacheNames {user:detail, user:list}。注意它与CachePut的区别CacheEvict只看缓存操作成功与否与方法的返回值没有约束关系所以删除方法写成 void 完全没问题。2.4 Caching一个方法要组合多个缓存操作时就靠它有些场景一个方法既想更新 A 缓存又想删除 B 缓存比如更新用户信息之后用户详情缓存要刷新用户列表缓存要清空。一个注解显然不够用Caching就是把多个缓存注解组合在一起的聚合注解。Caching( put { CachePut(cacheNames user:detail, key #user.id) }, evict { CacheEvict(cacheNames user:list, allEntries true) } ) public User updateUserAndRefreshCache(User user) { userMapper.updateById(user); return user; }Caching内部有三个数组属性cacheable放Cacheableput放CachePutevict放CacheEvict。组合起来的使用场景非常灵活。要注意的是放进去的每个注解都会独立生效它们的 key、condition、unless 都是独立配置的彼此不干扰。我见过的一个坑是有人在Caching里放多个CachePut结果 key 写成了同一个后面的覆盖前面的白写了一遍。组合注解在使用前要自己想清楚同一个缓存区域内多个 put 操作到底是想合并还是覆盖覆盖是默认行为所以 key 尽量不要重复。Caching看起来解决了很多复杂场景但我也要泼一盆冷水如果发现一个方法上的缓存注解组合超过三个以上缓存逻辑就有点复杂了优先考虑是不是缓存区域的划分有问题。把缓存区域设计得更细一点比如列表缓存和详情缓存分开管理大部分场景用一两个注解就够了。2.5 CacheConfig类级别配置减少重复代码前几个注解都是加在方法上的。如果一个类里所有方法都要用同一个缓存区域每个方法写一遍cacheNames user:detail就太啰嗦了。CacheConfig就是用来做类级别的公共配置的。CacheConfig(cacheNames user:detail, keyGenerator customKeyGenerator) Service public class UserService { Cacheable(key #id) public User getUserById(Long id) { ... } }CacheConfig支持的属性包括 cacheNames、keyGenerator、cacheManager、cacheResolver。类级别的配置生效后方法上的注解可以省略相同属性的配置。如果方法上也写了同样的属性方法级别的配置优先这种就近覆盖的原则和 Spring 的配置优先级思想一致。这里容易有一个理解偏差CacheConfig里写的 cacheNames 是“默认缓存区域”不是把类里所有缓存都用这个区域。方法上可以用Cacheable(cacheNames 其他区域)自己覆盖。如果方法上没写 cacheNames 而类上写了就沿用类上的如果两个都没写运行时会因为找不到缓存区域直接报错。顺带说一句CacheConfig的 keyGenerator 如果指定了它会让这个类里的所有缓存注解都用自定义生成器除非方法上单独指定 key 属性。key 和 keyGenerator 的互斥规则在这里同样生效类级别指定了 keyGenerator 后方法里千万不能再写 key否则启动会报配置冲突。2.6 三个核心注解的属性对比速查我整理了Cacheable、CachePut、CacheEvict最常用的属性对比方便日常查阅属性CacheableCachePutCacheEvict作用说明cacheNames / value必填必填必填缓存区域支持数组keySpEL 指定SpEL 指定SpEL 指定缓存 key默认按参数生成keyGenerator自定义 Bean自定义 Bean自定义 Bean与 key 互斥condition方法调用前判断方法调用前判断方法调用前判断为 true 才执行缓存逻辑unless方法调用后按返回值判断同左无为 true 则不缓存sync支持不支持无同步阻塞防止击穿allEntries无无支持清空整个缓存区域beforeInvocation无无支持方法调用前删缓存这个表格建议收藏面试问 Spring Cache 底层原理也能答出个大概Cacheable是“读多写少”场景的主力CachePut是“写后刷新”CacheEvict是“写后失效”。三者的核心区别就一句话Cacheable 可能不执行方法CachePut 一定执行方法CacheEvict 只清理不查询。3. 从零搭建一个基于 Redis 的 Spring Cache 实战示例3.1 场景设计与依赖搭建理论讲再多不如一个完整的例子直观。我拿一个典型的用户服务来说明。业务场景有三个接口按 ID 查询用户详情高频查询需要缓存更新用户信息需要刷新用户详情缓存同时清理用户列表缓存删除用户需要清理用户详情缓存这种场景在企业系统里非常普遍比如后台用户管理、会员中心、订单详情套路完全一致。基础环境用 Spring Boot 2.7 或 3.x 都可以Spring Cache 的注解 API 没有太大变化。先在 pom 里引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependencyRedis 连接信息在 application.yml 里配置基本的 host、port、password 按实际环境来。注意 Spring Boot 2.x 默认的 Redis 客户端是 Lettuce对于绝大多数业务系统性能足够了不需要额外引入 Jedis。启动类上加EnableCaching是必须的不加注解再全也不会有任何缓存行为。这个注解会导入缓存相关的自动配置并开启 AOP 代理这是整套机制生效的前提。SpringBootApplication EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }3.2 配置 RedisCacheManager解决序列化和过期时间EnableCaching只是开启能力具体用哪个缓存管理器需要配置。Spring Boot 的 Redis Cache 自动配置会生成一个默认的RedisCacheManager但默认配置有个大坑序列化方式用的是 JDK 序列化缓存到 Redis 里的值是一串二进制乱码肉眼不可读排查问题非常痛苦。所以项目落地时几乎都要自定义序列化。Configuration public class CacheConfig { Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .prefixCacheNameWith(myapp:) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.json())); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .withCacheConfiguration(user:list, RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5)) .prefixCacheNameWith(myapp:) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.json()))) .build(); } }这段配置里的几个细节值得单独说明。第一.prefixCacheNameWith(myapp:)会为所有 key 统一加上项目前缀这样不同应用共用 Redis 时不会互相覆盖。这个前缀会在实际 key 中体现比如myapp:user:detail::1注意 Spring Cache 默认用两个冒号分隔缓存区域和 key这是它的默认 key 格式。第二.entryTtl(Duration.ofMinutes(30))是全局默认过期时间。不同业务对过期时间的要求不同比如用户列表可能只需要 5 分钟的强一致用户详情可以放宽到 30 分钟。用.withCacheConfiguration为特定缓存区域单独覆盖 TTL 是推荐做法比所有缓存一刀切更合理。第三StringRedisSerializer序列化 keyRedisSerializer.json()序列化 value。这样 Redis 里存的是可读的 JSON 字符串对排查问题非常友好。需要注意JSON 序列化要求缓存的对象有可用的无参构造函数和 getter/setter绝大多数 POJO 都能满足。如果你的对象里有 LocalDateTime 这类时间类型建议在 ObjectMapper 里注册 JavaTimeModule否则序列化会报错这是一个容易踩的隐藏坑。3.3 Service 层的缓存注解实战依赖和配置就绪后写业务代码。用户实体public class User { private Long id; private String name; private Integer age; // getter/setter 省略 }UserService 核心逻辑Service public class UserService { Autowired private UserMapper userMapper; Cacheable(cacheNames user:detail, key #id, sync true) public User getUserById(Long id) { System.out.println(查询数据库获取用户 id); return userMapper.selectById(id); } Caching( put { CachePut(cacheNames user:detail, key #user.id) }, evict { CacheEvict(cacheNames user:list, allEntries true) } ) public User updateUser(User user) { userMapper.updateById(user); return user; } CacheEvict(cacheNames user:detail, key #id) public void deleteUser(Long id) { userMapper.deleteById(id); } }这套代码跑起来之后第一次调用getUserById(1)会打印“查询数据库获取用户1”第二次调用不会打印直接在 Redis 命中缓存。调用updateUser后用户详情缓存会被覆盖成最新数据同时用户列表缓存被整体清空。调用deleteUser后对应用户详情缓存被移除。这个示例的逻辑链路非常清晰推荐作为 Spring Cache 的入门模板。其中CachePut和CacheEvict组合通过Caching实现了“详情刷新 列表失效”真实项目中这种场景出现的频率极高直接抄作业就行。Controller 层就没什么特殊了正常的 RestController 调用 UserService 的方法即可。有一点值得提醒缓存注解要加在 Service 层方法上不要加到 Controller 方法上。原因在于 Service 层是业务逻辑的边界一个 Service 方法可能被多个 Controller 复用缓存写在 Service 上才能保证所有入口都命中同一份缓存。如果写在 Controller 上不同 Controller 的缓存互相独立等于缓存白做了。4. 缓存失效、穿透、序列化问题排查实录4.1 同类内部调用导致缓存注解失效这是 Spring Cache 使用中排名第一的暗坑很多老手都踩过。在同一个类里一个方法调用另一个被Cacheable标注的方法时注解不生效。Service public class UserService { public User getUserWithVerify(Long id) { // 这里的 getUserById 是 this.getUserById不经代理 User user getUserById(id); return user; } Cacheable(cacheNames user:detail, key #id) public User getUserById(Long id) { return userMapper.selectById(id); } }原因在于 Spring Cache 基于 AOP 代理实现代理对象只对“外部调用”生效。同类里的this.getUserById()直接走原对象的方法绕过了代理缓存逻辑自然被跳过。解决方式有三种第一种最直接把被缓存的方法拆到另一个 Bean 里比如建一个UserCacheService注入它再调用。这符合职责单一的思路也绕开了代理问题我推荐这种。第二种是注入自身代理Spring 支持Autowired Lazy注入一个本类代理到自己属性中然后通过代理调用。这种方式能解决同类自调用但会有循环依赖隐患代码可读性也一般。第三种是从 AopContext 获取代理需要启动EnableAspectJAutoProxy(exposeProxy true)然后((UserService) AopContext.currentProxy()).getUserById(id)。这个方法能在不改结构的情况下解决自调用但侵入性不小而且强制要求开启 exposeProxy平时不太建议。怎么排查这个问题最简单的方法是看日志如果第二次调用同一个 id 时方法里的 SQL 日志还是打出来了十有八九是走了自调用。4.2 缓存穿透null 结果不缓存恶意请求直接打穿库前一章提到unless #result null虽然符合直觉但它有个副作用恶意请求一个不存在的 id 时每次都会穿透缓存查数据库。攻击者只要循环传不存在的 id缓存就永远命中不了数据库压力直接拉满。这就是典型的缓存穿透。要解决穿透一个成熟的做法是把空值也缓存起来。但 Spring Cache 默认不缓存 null因为 null 代表“方法没有返回对象”。我们需要在代码层面把空值包一层或者借助Optional。Spring Cache 在较新版本中对Optional有特殊处理如果方法返回Optional.empty()框架会特殊标记不会对Optional本身做序列化但 null 依然不会缓存。我实际项目中更常用的方案是查询接口在 Service 内做一层约束当方法返回 null 时构造一个“空对象”返回并设置较短的 TTL。比如User为空时返回一个User对象id 为 null名称是空字符串缓存 3 到 5 分钟。这样至少把穿透放大效应限制住了代价是空对象会占用一点缓存空间但收益远大于成本。如果系统对缓存穿透特别敏感还可以在缓存前加一层布隆过滤器把所有存在的 id 预加载到过滤器里不存在直接拦截。布隆过滤器方案适合 id 集合稳定、查询量极大的场景引入成本稍高中小项目未必值得。还有一个细节Spring Cache 的condition和unless都能部分缓解穿透但它们严格来说只是“过滤”不是“防御”。真正要防穿透靠的还是空值缓存和过滤器这类工程手段。4.3 缓存与数据库一致性先删缓存还是先更库用 Spring Cache 注解天然会面对一致性问题。一个经典场景更新用户信息时如果直接CacheEvict删除缓存但数据库更新失败缓存被删了下次查询会重新加载旧数据然后缓存里又是旧值风险不大反过来如果数据库更新成功但缓存删除失败那缓存里就一直留着旧数据用户看到的是过期信息。解决一致性的主流思路是“先更新数据库再删除缓存”这也是很多公司内部推广的准则。Spring Cache 默认的CacheEvict行为是方法成功执行后删除缓存在纯 Service 场景下基本能满足这个顺序。但一旦方法加了Transactional事务事情就变复杂了。因为 Spring Cache 的缓存清理发生在方法返回后、事务提交前还是提交后存在时序问题。如果缓存删除在事务提交前完成删除后事务又回滚了此时缓存被删数据库还是旧值下一次查询加载回旧数据一致性被轻微破坏问题不大更大的隐患是缓存删除发生在事务提交之前事务还没提交其他线程已经读到旧缓存不可控窗口被拉长。更稳妥的做法是把缓存操作纳入事务同步。Spring 提供了TransactionAwareCacheManagerDecorator它把 CacheManager 包装一层能让缓存操作延迟到事务提交之后再执行保证“数据库先变、缓存后变”。用法很简单Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheManager base ...; return new TransactionAwareCacheManagerDecorator(base); }这个细节很容易被忽略但事务和缓存同时出现时强烈建议加上。另一个被讨论得比较多的是“延迟双删”先删缓存再更新数据库再延迟几百毫秒删一次缓存。这种做法能解决并发场景下旧缓存被回填的问题但实现成本高、延迟时间不好确定我一般不建议在中小项目里硬上。先更新库再删缓存配合 Redis 主从同步和短暂的缓存过期时间已经能满足绝大多数业务的一致性要求了。4.4 Redis 里的 Key 是乱码如何配置序列化器这个问题的定位成本很低看到 Redis 里 key 变成\xAC\xED\x00\x05t\x00开头的一串乱码基本就是用了默认的 JDK 序列化。JDK 序列化既浪费空间又不可读线上想手动删一个 key 都难以下手。解决办法就是第三章已经展示过的配置key 用StringRedisSerializervalue 用 JSON 序列化器。注意两个序列化器要同时设置只设置其中一个容易报错。有些文章只贴了serializeValuesWith忽略 key 的序列化器结果 key 仍是乱的因为配置没全改。如果项目里用的是 Spring Boot启动日志有时候会显示默认RedisCacheManager说明你的自定义配置没有生效。排查顺序是确认是否配置类被扫描到、确认Bean方法返回类型是否为RedisCacheManager、确认配置类上没有多余的ConditionalOnMissingBean干扰。JSON 序列化后的 value 在 Redis 里是一段 JSON 字符串比如{id:1,name:张三,age:20}看到这种数据才说明配置成功。还有一个冷门提醒JSON 序列化会把类型信息写进缓存如果前后端对象的包名或字段发生变化旧缓存里的类型信息和当前代码对不上反序列化可能报错。为了平滑升级一般要控制好缓存 TTL或者上线前做一次缓存清空。4.5 常见问题速查表与几个实战习惯再花点篇幅把这类问题整理成速查表方便复制到项目 Wiki 里现象可能原因解决思路缓存注解完全不生效启动类没加 EnableCaching检查注解和 AOP 代理同类调用不掉缓存自调用绕过代理拆 Bean、自注入代理key 是乱码JDK 序列化配置 StringRedisSerializervalue 不可读默认 JSON 配置覆盖不彻底检查 RedisCacheManager 自定义缓存穿透打爆 DBnull 不缓存缓存空对象、布隆过滤器更新后缓存还是旧的缓存删除失败或顺序不对先更库再删缓存、事务同步多个线程查同一个 key未开启 sync设置 sync true缓存区域 key 冲突没有业务前缀用 cacheNames 区分 prefixCacheNameWith最后分享几个我长期维护项目时沉淀下来的习惯。第一个所有 cacheNames 统一使用“业务域:子域”的命名风格比如user:detail、order:list前缀和项目名无关只和业务有关这样清理缓存时一眼能分辨归属。第二个给每一个缓存区域想清楚 TTL不设置 TTL 的缓存基本等于慢性毒药数据变更逻辑一旦漏掉脏数据能存活到地老天荒。第三条代码 review 阶段如果发现一个方法上的缓存注解超过三个先停下来讨论一下缓存区域设计是不是有问题而不是直接让 Caching 把逻辑堆上去。第四条也是最重要的一条——多环境一定要区分缓存隔离。测试环境可以配置spring.cache.typenone直接关闭缓存让测试同事不受残留缓存干扰上线时再开启。Spring Cache 的抽象层让这个开关变得异常简单用一个配置项就能切换“全缓存”和“无缓存”两种模式这是手写缓存代码永远给不了你的便利。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑