@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 兜底、提交后再删;自调用不生效、序列化要统一。
笔记加载中…