2. 单体架构下的缓存模式:本地缓存与集中式缓存的典型架构、单体架构下的缓存一致性保障机制
2.1 单体架构的缓存拓扑
在单体应用(Monolithic Application)中,所有业务逻辑、数据访问和缓存组件都运行在同一个进程或同一组紧密耦合的服务器内。根据缓存数据的存储位置与共享范围,可划分为两种典型模式:本地缓存(Local Cache)与集中式缓存(Centralized Cache)strong>。
核心区别:本地缓存在每个应用实例的堆内存中独立存储数据,实例之间不共享缓存空间;集中式缓存将数据统一存放在独立的缓存服务中,所有应用实例通过远程协议访问同一份缓存数据。
2.2 本地缓存模式
2.2.1 典型架构
本地缓存通常采用 JVM 堆内缓存(如 HashMap、Guava Cache、Caffeine)或 堆外缓存(如 Ehcache 的 off-heap 模式)。数据直接存储在应用进程内部,访问延迟极低(纳秒级),无需网络开销。
| 特性 | 本地缓存 | 集中式缓存 |
|---|---|---|
| 数据存储位置 | 应用进程堆内/堆外 | 独立缓存服务(如 Redis、Memcached) |
| 访问延迟 | 纳秒级(无网络) | 微秒~毫秒级(网络 RTT) |
| 数据共享 | 仅限单实例内部 | 跨实例全局共享 |
| 容量上限 | 受限于单机堆内存 | 可水平扩展至 TB 级 |
| 典型实现 | Caffeine, Guava, Ehcache | Redis, Memcached, Hazelcast |
2.2.2 本地缓存一致性保障
在单体架构中,本地缓存的一致性挑战主要来自同一进程内多个线程对缓存数据的并发读写,以及缓存与数据库之间的数据同步。常用保障机制包括:
- 显式失效(Explicit Invalidation):数据更新后,立即调用缓存 API 删除或更新对应 key。
- TTL / 过期策略:设置合理的过期时间,容忍短暂的不一致窗口。
- 读写锁 / 分段锁:对热点 key 加锁,防止缓存击穿和并发更新丢失。
- Cache-Aside 模式:读时先查缓存,miss 则查库并回填;写时先更新库,再删除缓存。
// Cache-Aside 模式示例(伪代码)
public User getUser(String userId) {
User user = localCache.get(userId);
if (user == null) {
synchronized (this) {
user = localCache.get(userId); // double-check
if (user == null) {
user = userDao.findById(userId);
localCache.put(userId, user, Duration.ofMinutes(5));
}
}
}
return user;
}
public void updateUser(User user) {
userDao.update(user);
localCache.invalidate(user.getId()); // 先更新库,再删缓存
}
实践建议:单体应用内使用本地缓存时,务必为每个缓存 key 设置合理的 TTL,避免因程序 bug 导致“缓存永久污染”。推荐使用 Caffeine 作为本地缓存实现,其内置的 W-TinyLFU 淘汰算法和异步刷新机制能有效降低不一致窗口。
2.3 集中式缓存模式
2.3.1 典型架构
集中式缓存将缓存层独立部署,所有应用实例通过 TCP/Redis 协议等访问同一缓存集群。单体架构下通常部署 1~3 个 Redis 节点(主从或哨兵模式),应用通过连接池复用长连接。
2.3.2 集中式缓存一致性保障
集中式缓存模式下,一致性风险主要来自应用本地缓存与集中式缓存之间的数据不一致,以及集中式缓存与数据库之间的数据不一致。单体架构下常用的保障机制包括:
- 双删策略(Double Delete):更新数据库前删除一次缓存,更新后再延迟删除一次,补偿并发读请求回填的脏数据。
- 订阅 binlog 异步同步:通过 Canal 等组件监听数据库变更,主动推送失效消息到缓存。
- 读写穿透(Read/Write Through):缓存层封装数据加载与持久化逻辑,应用只与缓存交互。
- 版本号 / 时间戳校验:缓存中存储数据版本号,每次读取时校验,版本落后则重新加载。
// 双删策略示例(伪代码)
public void updateUserWithDoubleDelete(User user) {
// 第一次删除
redisCache.delete("user:" + user.getId());
// 更新数据库
userDao.update(user);
// 延迟 500ms 后第二次删除(补偿并发回填)
executor.schedule(() -> {
redisCache.delete("user:" + user.getId());
}, 500, TimeUnit.MILLISECONDS);
}
注意:双删策略中的延迟时间需要根据业务并发量和数据库响应时间合理设置。过短无法补偿并发回填,过长则增加不一致窗口。建议延迟时间 = 平均数据库查询耗时 × 2 + 100ms 缓冲。
2.4 两种模式的对比与选型
| 对比维度 | 本地缓存 | 集中式缓存 |
|---|---|---|
| 一致性保障难度 | 低(单进程内锁机制成熟) | 中高(需处理网络延迟、并发回填) |
| 数据共享能力 | 无(实例间隔离) | 强(全局统一视图) |
| 运维复杂度 | 低(无额外组件) | 中(需维护缓存集群、哨兵/集群模式) |
| 适用场景 | 配置信息、字典数据、只读热点 | 用户会话、商品库存、全局计数器 |
| 单体架构推荐策略 | 一级缓存(优先使用) | 二级缓存(本地 miss 后回源) |
单体架构最佳实践:推荐采用 本地缓存 + 集中式缓存 的二级缓存模式。本地缓存承载高频只读热点,集中式缓存作为全局共享层兜底。一致性保障上,本地缓存设置短 TTL(如 1~5 分钟),集中式缓存采用双删或 binlog 同步,兼顾性能与一致性。
2.5 单体架构下的缓存一致性保障机制总结
单体架构下,缓存一致性保障的核心在于明确数据更新路径和控制并发回填窗口。以下为常用机制对比:
| 机制 | 一致性强度 | 性能影响 | 实现复杂度 | 典型场景 |
|---|---|---|---|---|
| Cache-Aside + 显式失效 | 最终一致 | 低 | 低 | 通用读写场景 |
| 双删策略 | 最终一致(窗口可控) | 中(额外一次删除) | 中 | 高并发写热点 |
| binlog 异步同步 | 最终一致(延迟秒级) | 低(异步) | 高 | 对一致性要求较高的核心数据 |
| 读写穿透 | 强一致(缓存层封装) | 中(缓存层增加逻辑) | 高 | 需要简化应用侧缓存逻辑 |
| 版本号校验 | 最终一致(可检测冲突) | 中(额外版本查询) | 中 | 乐观锁场景 |
选型建议:单体架构初期,优先采用 Cache-Aside + 显式失效 + 合理 TTL,足以应对 80% 的场景。当出现缓存与数据库长时间不一致时,再升级为双删策略或引入 binlog 同步。切勿过度设计,保持架构简洁。