第2章:模块间缓存共享的几种模式

2.1 三种模式概述

在多模块架构中,每个模块可能维护自己的本地缓存(如Caffeine、Guava Cache),也可能依赖中央缓存(如Redis),或者在缓存失效时通过消息总线通知其他模块。本章对比三种主流模式:本地缓存穿透、分布式缓存统一、消息驱动同步。

关键理解:选择哪种模式取决于模块间的边界隔离程度、一致性要求、网络开销容忍度。

2.2 三种模式对比表

模式核心思想一致性模块耦合典型场景常见实现
本地缓存穿透单模块本地缓存未命中时,穿透到下游共享存储最终一致性(依赖本地TTL)低读多写少,容忍短暂不一致Caffeine + DB
分布式缓存统一所有模块共用同一中央缓存,无本地缓存强一致性(单点写入)高严格一致,写操作频率低Redis Cluster
消息驱动同步缓存变更时发送消息,其他模块监听并更新本地缓存最终一致性(消息时序保证)中写频繁,模块独立部署RabbitMQ + 本地缓存失效
实践建议:优先考虑本地缓存穿透模式,因为它模块隔离最强;仅当一致性要求极高时才使用分布式缓存统一。

2.3 模式一:本地缓存穿透(Local Cache Penetrate)

每个模块拥有独立的本地缓存实例。当请求在本地缓存中未命中时,查询下游共享存储(如数据库或远程服务),并将结果回填到本地缓存。模块间没有直接的缓存共享机制。

  • 优点:模块完全隔离,无网络依赖;本地缓存极快(纳秒级)。
  • 缺点:数据一致性较弱,同一数据在不同模块中可能因TTL不同而短暂不一致;每个模块都需要处理穿透逻辑。
  • 实现示例(Spring Boot + Caffeine):
@Cacheable(value = "product", key = "#id")
public Product getProduct(Long id) {
    // 若本地缓存未命中,调用数据库查询
    return productRepository.findById(id).orElse(null);
}
常见错误:不同模块使用不同的本地缓存TTL,且未设置合理的过期策略,导致数据库被大量穿透请求击垮。建议所有模块统一TTL下限,并增加熔断机制。

2.4 模式二:分布式缓存统一(Centralized Distributed Cache)

所有模块访问同一个分布式缓存(如Redis)。模块自身不维护本地缓存,每次请求都通过网络访问中央缓存。中央缓存是唯一的数据源。

  • 优点:强一致性,所有模块读到同一份数据;缓存管理集中化。
  • 缺点:模块与缓存紧耦合,网络延迟增加;中央缓存成为单点瓶颈(需集群化)。
  • 实现示例(Spring Data Redis):
@Autowired
private StringRedisTemplate redisTemplate;

public String getConfig(String key) {
    return redisTemplate.opsForValue().get(key);
}

public void setConfig(String key, String value) {
    redisTemplate.opsForValue().set(key, value);
}
性能优化:使用Redis Pipeline或Lua脚本批量操作,减少网络往返次数;为热点key添加本地二级缓存(但会引入一致性问题)。

2.5 模式三:消息驱动同步(Message-Driven Sync)

当某个模块更新缓存(或底层数据)时,发送一条失效/更新消息到消息中间件(如RabbitMQ、Kafka)。其他模块监听该消息,并主动清除或刷新自己的本地缓存。

  • 优点:模块保持本地缓存高速访问,同时通过异步消息实现最终一致性;耦合度中等。
  • 缺点:增加消息中间件依赖;消息丢失或重复可能导致不一致,需幂等处理。
  • 实现流程:
  1. 模块A更新数据后,发送CacheInvalidateEvent到Exchange。
  2. 模块B的消费者监听该事件,调用本地缓存的evict(key)。
  3. 下次模块B查询时重新穿透加载最新数据。
// 发送消息(在写操作后)
@Autowired
private RabbitTemplate rabbitTemplate;

public void updateProduct(Product product) {
    productRepository.save(product);
    rabbitTemplate.convertAndSend("cache.exchange", "cache.invalidate",
            new CacheInvalidateEvent("product", product.getId()));
}

// 消费者
@RabbitListener(queues = "cache.invalidate.queue")
public void handleInvalidate(CacheInvalidateEvent event) {
    String cacheName = event.getCacheName();
    Object key = event.getKey();
    cacheManager.getCache(cacheName).evict(key);
}
关键点:消息队列应保证至少一次投递,消费者需幂等(例如通过事件ID去重)。建议使用死信队列处理失败消息。

2.6 三种模式结构图

下图展示了三种模式在模块A、模块B和共享存储之间的数据流差异:

三种缓存共享模式对比图 模块A 模块B 共享存储 本地缓存A 本地缓存B 数据库/RPC 穿透 穿透 模块A(无本地缓存) 模块B(无本地缓存) Redis中央缓存 回源数据库 模块A本地缓存 模块B本地缓存 消息队列 写时发消息 广播失效 模式选择建议 • 本地缓存穿透:模块间无依赖,适合独立部署、读多写少场景。 • 分布式缓存统一:强一致性但耦合高,适合小规模、写少场景。 • 消息驱动同步:折中方案,适合写频繁、需最终一致的微服务架构。

2.7 选择决策流程

  1. 是否允许最终一致性? 是 → 走2;否 → 走分布式缓存统一。
  2. 写操作频率高吗? 高 → 消息驱动同步;低 → 本地缓存穿透。
  3. 模块间网络延迟是否敏感? 是 → 优先本地缓存穿透+异步通知。
  4. 团队运维能力是否成熟? 不成熟 → 本地缓存穿透最简单;成熟 → 可考虑消息驱动。
实战微调:也可混合使用,例如热点数据用分布式缓存统一,非热点用本地缓存穿透+简单TTL。

2.8 常见错误与排查

错误现象可能原因解决方案
本地缓存穿透导致数据库压力陡增TTL设置过长或未设置,多个模块同时穿透统一设置TTL上限,使用缓存预热 + 布隆过滤器
分布式缓存统一中缓存雪崩大量key同时过期设置随机过期时间,使用本地缓存做二级保护
消息驱动同步中缓存不一致消息丢失或重复消费开启消费端手动ACK,业务幂等去重

2.9 性能与稳定性优化要点

  • 本地缓存穿透:使用Caffeine的expireAfterWrite和maximumSize限制内存;加锁防止缓存击穿(如@Cacheable(sync=true))。
  • 分布式缓存统一:客户端连接池配置,避免频繁创建连接;监控Redis内存和命中率,及时调整最大内存策略。
  • 消息驱动同步:消息积压时降级为直接查询数据库;设置消息TTL,过期自动丢弃,避免死信堆积。

2.10 本章小结

三种模式各有适用边界:本地缓存穿透保证模块独立,适合单元部署;分布式缓存统一提供强一致,适合小规模系统;消息驱动同步在一致性与解耦间取得平衡。实际项目中可组合使用,但务必明确缓存同步的边界与一致性等级,避免过度设计。