@Cacheable 缓存抽象原理是什么?如何保证缓存与数据库一致性?
结论先行:Spring Cache 是缓存抽象层:@Cacheable 用 AOP 拦截方法——命中缓存直接返回、未命中执行方法并把结果写入缓存;真正的存储由 CacheManager 决定(内存/Redis/Caffeine)。一致性上 Cache Aside 是主流:读缓存→未命中读库→回填;写库后删缓存(而非更新缓存),配合延迟双删/过期时间兜底,解决并发下的陈旧数据。
一、注解与原理
@Cacheable(cacheNames = "user", key = "#id")
public User getUser(Long id) { return userMapper.select(id); }
@CacheEvict(cacheNames = "user", key = "#id") // 删除缓存
public void updateUser(Long id, User u) { ... }
@CachePut(cacheNames = "user", key = "#user.id") // 更新缓存(方法总会执行)
public User save(User user) { ... }
| 注解 | 行为 | 典型场景 |
|---|---|---|
| @Cacheable | 命中即返回,未命中执行并缓存 | 读多写少查询 |
| @CachePut | 总是执行并把结果写缓存 | 写后同步刷新 |
| @CacheEvict | 执行后/前删除缓存 | 更新/删除后失效 |
| @Caching | 组合上面多个注解 | 多缓存名同步维护 |
原理:@EnableCaching 注册 CacheInterceptor(AOP 环绕),方法通过代理调用才生效——类内自调用同样会失效。
二、启用 Redis 缓存
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration cfg = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 全局 TTL
.serializeValuesWith(SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(cf).cacheDefaults(cfg).build();
}
- 依赖 spring-boot-starter-cache + spring-boot-starter-data-redis,启动类加 @EnableCaching;
- key 默认由参数生成,多用 SpEL 显式指定;注意序列化器一致性(避免 JDK 默认二进制不可读);
- 缓存 null:@Cacheable 默认不缓存 null(除非配置允许),需防缓存穿透自行处理。
三、缓存与 DB 一致性方案
- 首选 Cache Aside:更新 DB 成功后删除缓存,下次读再回填;
- 并发窗口:删缓存瞬间恰有读请求回填旧值 → 用延迟双删(写后删→短暂延迟→再删一次)收窄窗口;
- 过期时间兜底:任何方案都必须配 TTL,防止极端情况下永久脏数据;
- 强一致替代:缓存只读最新(如订阅 binlog 同步),或对一致性要求高的数据直接不缓存;
- 缓存与事务:删除动作放在事务提交后(TransactionSynchronizationManager afterCommit),避免回滚后删错。
常见追问 / 记忆点
- 追问:为什么“先更新缓存再写库”不行?答:写库失败或并发读旧写新都会造成缓存与库不一致,删除缓存更安全。
- 追问:缓存穿透/击穿/雪崩与 @Cacheable 有关吗?答:有关,缓存空值+布隆过滤、互斥重建、TTL 抖动分别应对三种故障。
- 记忆点:读回填、写删除、TTL 兜底、提交后再删;自调用不生效、序列化要统一。