SpringNotes

第 12 章:缓存

zjc 于 2026-01-12 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 缓存用于降低延迟、减少数据库压力、保存临时计算结果。它同时引入一致性问题、过期策略、击穿穿透雪崩和容量治理。是否加缓存,应先量化慢在哪里。

12.1 缓存层次

请求
  -> 本地缓存
  -> 分布式缓存 Redis
  -> 数据库
缓存 特点
本地 Caffeine 低延迟、进程内、容量受 JVM 堆限制
Redis 多实例共享、容量独立、网络开销
多级缓存 高性能但一致性和失效更复杂
CDN/网关缓存 静态或准静态内容

多数系统不应一开始就做复杂多级缓存。先 Redis 或 Caffeine,遇到明确收益再演进。

12.2 Spring Cache 抽象

启用:

@EnableCaching
@SpringBootApplication
public class Application {
}

使用:

@Service
public class ProductService {

    @Cacheable(cacheNames = "products", key = "#id")
    public ProductView get(Long id) {
        return repository.findView(id);
    }

    @CacheEvict(cacheNames = "products", key = "#id")
    public void update(Long id, UpdateProductCommand command) {
        repository.update(id, command);
    }
}

常用注解:

注解 作用
@Cacheable 先查缓存,未命中执行方法后写入
@CachePut 执行方法并更新缓存
@CacheEvict 删除缓存
@Caching 组合多个缓存操作
@CacheConfig 类级公共配置

12.3 key 生成

默认 key 基于参数生成。复杂场景应显式:

@Cacheable(cacheNames = "products", key = "'user:' + #userId + ':page:' + #page")
public Page<ProductView> page(Long userId, int page) {
}

注意:

  1. key 必须稳定序列化;
  2. 参数对象应实现 equals/hashCode;
  3. key 要包含命名空间和版本;
  4. 避免无界 key;
  5. key 中不要包含敏感明文。

12.4 RedisCacheManager

@Configuration
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues()
                .computePrefixWith(name -> "app:" + name + ":")
                .serializeValuesWith(SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()));

        Map<String, RedisCacheConfiguration> configs = Map.of(
                "products", config.entryTtl(Duration.ofMinutes(5)),
                "configs", config.entryTtl(Duration.ofHours(1)));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .withInitialCacheConfigurations(configs)
                .build();
    }
}

序列化选择:

方式 特点
JDK 序列化 简单但可读性差、跨版本风险
JSON 可读,需处理类型和兼容
GenericJackson2JsonRedisSerializer 带类型信息,注意类型 ID 风险
自定义 DTO 序列化 契约清晰,推荐核心数据

12.5 一致性策略

常见顺序:

先更新数据库,再删除缓存
@Transactional
public void update(Long id, UpdateCommand command) {
    repository.update(id, command);
    cache.evict("product:" + id);
}

为什么常删缓存而不是更新:

  1. 避免并发写覆盖;
  2. 懒加载下次重建;
  3. 避免写冷数据;
  4. 删除语义简单。

但该策略仍可能出现短暂不一致。更强方案:

  1. 延迟双删;
  2. 版本号;
  3. TTL 兜底;
  4. 订阅 binlog/CDC 删除;
  5. 读写锁或串行化;
  6. 强一致读数据库。

12.6 缓存问题

问题 定义 应对
穿透 查询不存在的数据 缓存空值、布隆过滤器、参数校验
击穿 热 key 过期瞬间大量请求 互斥重建、逻辑过期、热点永不过期
雪崩 大量 key 同时过期 TTL 加随机、限流、熔断
污染 缓存错误数据 校验来源、版本、灰度
膨胀 key 无界增长 最大容量、淘汰策略

互斥重建示例:

public Product get(Long id) {
    String key = "product:" + id;
    Product cached = redis.get(key);
    if (cached != null) {
        return cached;
    }

    String lockKey = "lock:" + key;
    if (redis.setNx(lockKey, "1", Duration.ofSeconds(3))) {
        try {
            Product product = repository.find(id);
            redis.set(key, product, ttlWithJitter());
            return product;
        } finally {
            redis.delete(lockKey);
        }
    }
    return repository.find(id);
}

12.7 TTL 与淘汰

Redis 配置:

maxmemory 2gb
maxmemory-policy allkeys-lru
策略 说明
noeviction 内存满拒绝写入
allkeys-lru 所有 key 近似 LRU
volatile-lru 只淘汰有 TTL 的 key
allkeys-lfu 近似 LFU
volatile-ttl 淘汰剩余时间短的 key

应用侧建议:

  1. 每类数据单独 TTL;
  2. 基础 TTL 加随机偏移;
  3. 热点数据可逻辑过期;
  4. 定期统计 key 数量和内存;
  5. 不允许“永久 + 无界”的业务 key。

12.8 Caffeine

@Bean
public Cache<Long, ProductView> productCache() {
    return Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(Duration.ofMinutes(5))
            .recordStats()
            .build();
}

使用:

public ProductView get(Long id) {
    return cache.get(id, key -> repository.findView(key));
}

指标:

hitCount
missCount
hitRate
evictionCount
loadAverageTime

本地缓存适合:

  1. 读取热点;
  2. 数据可短暂不一致;
  3. 单实例或可广播失效;
  4. 数据量有限。

12.9 多级缓存

L1 Caffeine
  -> L2 Redis
     -> DB

失效:

DB 更新
  -> 删除 Redis
  -> 发布失效消息
  -> 各实例删除本地 L1

问题:

  1. 消息丢失;
  2. 实例处理延迟;
  3. 时钟不同;
  4. 本地容量抖动;
  5. 版本不一致。

多级缓存通常只用于极热点数据,且必须监控命中率和不一致窗口。

12.10 缓存治理

上线前检查:

1. key 命名规范
2. 最大 TTL 和最小 TTL
3. 容量和淘汰策略
4. 空值处理
5. 热点保护
6. 序列化兼容
7. 权限和网络隔离
8. 命中率指标
9. 回源限流
10. 回滚方案

监控:

cache_hit_rate
cache_miss_count
cache_eviction_count
cache_get_latency
cache_put_latency
cache_redis_pool_active
cache_redis_errors
origin_query_count

命中率低时要分析:

  1. key 是否随机化过强;
  2. TTL 是否过短;
  3. 容量是否不足;
  4. 业务是否本身冷数据多;
  5. 是否值得缓存。

本章小结

Spring Cache 提供统一缓存抽象,底层可用 Caffeine、Redis 等。生产缓存的关键不是注解,而是 key 设计、TTL、容量、一致性、穿透击穿雪崩和命中率监控。先量化收益再引入缓存,并保留数据库兜底和回滚能力。

思考题

  1. 为什么常使用“更新数据库后删除缓存”?
  2. 缓存穿透、击穿、雪崩分别如何应对?
  3. 本地缓存和分布式缓存适合什么数据?
  4. 多级缓存的一致性难点是什么?
  5. 缓存命中率低时如何分析?