1.1 多模块项目缓存架构概览:缓存分层、同步需求、边界隔离的核心挑战
在多模块微服务架构中,缓存不再是一个独立组件,而是一套横跨各模块的分层体系。本章从全局视角梳理缓存分层的典型模型、模块间缓存同步的触发条件,以及模块边界隔离的设计约束。理解这三者是后续章节实现一致性协议、选择失效模式、配置缓存中间件的前提。
1.1.1 缓存分层模型
典型的微服务缓存架构自顶向下分为三层:
- 本地缓存层(L1):每个业务模块内部的进程内存缓存(如 Caffeine、Guava Cache),延迟极低(<1μs),但容量受限、数据不共享。
- 分布式缓存层(L2):跨模块共享的集中式缓存(如 Redis、Memcached),数据可被多个模块读取,但网络开销(~1ms)和序列化成本不可忽略。
- 数据库层(L3):持久化存储(MySQL、PostgreSQL),为数据一致性提供最终保障,访问延迟通常 >10ms。
下图展示了三层缓存在一个包含两个业务模块的系统中的关系与同步路径:
1.1.2 缓存同步需求
当模块A更新了数据,需要通知其他模块的本地缓存以及分布式缓存层,否则会出现“写后读不一致”。常见的同步触发场景包括:
| 场景 | 触发动作 | 同步目标 |
|---|---|---|
| 模块内数据变更 | 写操作后主动失效本地键 | 仅本模块本地缓存 |
| 模块间数据共享 | 通过消息队列广播变更事件 | 其他模块本地缓存 + 分布式缓存 |
| 分布式缓存过期 | TTL 到期或被驱逐 | 无需主动同步,但模块需回源数据库 |
| 数据库直写触发 | 监听 binlog 或 CDC 管道 | 分布式缓存(写回或失效) |
1.1.3 模块边界隔离的核心挑战
微服务要求每个模块拥有独立的数据域和缓存空间,但现实中的缓存往往被“无意识共享”,导致以下问题:
- 缓存键污染:模块A和模块B使用相同的缓存键前缀,导致互相覆盖。解决方案:模块级命名空间隔离(如
moduleA:user:123vsmoduleB:user:123)。 - 不当的缓存穿透:一个模块的错误查询(如空值未缓存)可能穿透到数据库,拖慢所有模块的DB连接池。
- 同步风暴:模块间过度广播无效变更事件,导致本地缓存频繁失效,命中率骤降。
- 序列化协议不一致:模块A使用 JSON,模块B使用 Protobuf,共用 Redis 时反序列化失败。
边界隔离设计原则:每个模块只应操作自己的缓存 namespace,通过统一 API Gateway 和事件总线处理跨模块缓存失效。严禁模块之间直接读写对方缓存命名空间。
1.1.4 常见错误与应对
| 错误模式 | 后果 | 应对方法 |
|---|---|---|
| 所有模块共用同一 Redis DB 且无键前缀 | 键碰撞、数据混乱 | 配置 keyPrefix = moduleName + ":" |
| 本地缓存未设置最大容量和淘汰策略 | OOM 或 GC 停顿 | 使用 Caffeine 设置 maximumSize 和 expireAfterWrite |
| 同步事件使用同步 RPC 而非异步消息 | 写请求阻塞、级联超时 | 改为 MQ(RocketMQ/Kafka)异步发布事件 |
| 忽略缓存穿透保护 | 数据库压力骤增 | 缓存空值 + Bloom Filter 预检 |
1.1.5 一个典型的模块缓存配置示例(Spring Boot + Redis)
以下配置展示了如何为每个模块隔离缓存键空间,并开启同步事件监听:
# application-moduleA.yml
spring:
cache:
type: redis
cache-names: moduleA:users, moduleA:products
redis:
time-to-live: 600s
redis:
namespace: moduleA # 自动添加前缀
# 同时需要在模块A中配置本地缓存(Caffeine)
# 用于热点数据降级,减少Redis压力
// CacheSyncEventPublisher.java
@Component
public class CacheSyncEventPublisher {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void notifyInvalidate(String keySpace, String key) {
// 发布到模块专属主题,其他模块订阅
String topic = "cache:invalidate:" + keySpace;
redisTemplate.convertAndSend(topic, key);
}
}
调试提示:在开发阶段,建议开启 Redis 命令日志(
REDIS_DEBUG=true)并观察 keys * 输出中的前缀,快速定位模块间键冲突。
1.1.6 性能与稳定性优化方向
- 本地缓存使用 Caffeine
weakKeys避免内存泄露,recordStats监控命中率。 - 分布式缓存采用 读写分离 + 本地缓存失效通知,减少对 Redis 的重复查询。
- 模块边界通过 API 版本化(如 /v1/moduleA/cache) 和 缓存事件契约(事件 schema 强制校验) 实现防御式隔离。
- 部署时每个模块独占 Redis 逻辑数据库(db0~db15) 或 独立 Redis 实例,物理隔离避免相互影响。