2. 单体架构下的缓存模式:本地缓存与数据库直连
在单体架构(Monolithic Architecture)阶段,所有业务逻辑、数据访问和缓存管理都运行在同一个进程内。这一阶段的缓存策略以“简单、直接”为主要特征,但同时也埋下了数据一致性的隐患。本章将剖析三种核心模式:本地缓存(HashMap/Guava)、缓存与数据库直连,以及它们带来的一致性挑战。
2.1 本地缓存实现:从 HashMap 到 Guava Cache
本地缓存是指将数据存储在应用进程的内存中,所有请求共享同一份缓存数据。在单体应用中,最常见的实现方式有两种:
| 实现方式 | 核心机制 | 典型代码 | 适用场景 |
|---|---|---|---|
| HashMap 缓存 | 使用 ConcurrentHashMap 手动管理键值对,无过期策略 |
private static final Map<String, Object> cache = new ConcurrentHashMap<>(); |
极简原型、配置项缓存、临时数据 |
| Guava Cache | 基于 CacheBuilder 构建,支持过期、最大容量、弱引用 |
Cache<String, Object> cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).maximumSize(1000).build(); |
高频读取、可容忍短暂不一致、本地会话 |
关键区别: HashMap 缓存需要开发者手动处理并发、过期和淘汰逻辑;Guava Cache 内置了 LRU 淘汰、定时过期、统计监控等能力,是单体架构下本地缓存的推荐选型。
2.2 缓存与数据库直连模式
在单体架构中,缓存与数据库的交互通常采用“直连模式”(Direct-Access Pattern),即应用层直接调用缓存 API 和数据库 DAO,没有中间代理层。其典型流程如下:
- 读操作: 先查询本地缓存(Cache-Aside 模式),命中则直接返回;未命中则查询数据库,将结果写入缓存后返回。
- 写操作: 先更新数据库,再删除或更新缓存(通常选择删除缓存,避免并发写导致的数据不一致)。
- 失效策略: 依赖 TTL(Time-To-Live)或手动调用
invalidate()方法。
实践建议: 单体架构下推荐使用“先更新数据库,再删除缓存”的策略(Cache-Aside + Delete),而非“先更新缓存,再更新数据库”,后者更容易出现脏数据。
2.3 单体架构的缓存一致性挑战
尽管本地缓存 + 直连模式实现简单,但在单体架构中,缓存与数据库之间的数据一致性面临以下三大核心挑战:
- 并发写冲突: 两个线程同时执行“更新数据库 + 删除缓存”操作,可能因时序交错导致缓存中残留旧数据。例如:线程A更新DB为v2,线程B更新DB为v3,线程A删除缓存后,线程B又写入v2(旧值)。
- 读扩散与缓存穿透: 热点数据失效瞬间,大量请求同时穿透到数据库,导致数据库压力骤增,甚至引发雪崩。
- 本地缓存无法跨进程共享: 单体架构虽然只有一个进程,但若部署多实例(如多容器),每个实例的本地缓存相互独立,导致同一用户在不同实例上看到不同数据。
典型陷阱: 很多团队在单体架构中使用“先更新缓存,再异步更新数据库”的策略,认为这样可以提升写性能。但一旦缓存更新成功而数据库更新失败,系统将长期提供错误数据,且难以自动恢复。这种模式在单体架构中应严格避免。
2.4 单体架构下的缓存一致性策略对比
针对上述挑战,单体架构中常用的缓存一致性策略及其适用性如下表所示:
| 策略名称 | 实现方式 | 一致性级别 | 适用场景 | 风险点 |
|---|---|---|---|---|
| Cache-Aside + 删除 | 读时回写,写时先更新DB再删缓存 | 最终一致性 | 读多写少,可容忍短暂不一致 | 并发写可能残留旧值 |
| Cache-Aside + 更新 | 写时同时更新DB和缓存 | 弱一致性 | 写频率低,缓存数据可覆盖 | 并发写导致缓存与DB数据错位 |
| Read-Through + TTL | 缓存层自动加载,TTL到期失效 | 最终一致性(延迟取决于TTL) | 数据变化不频繁,可接受延迟 | TTL期间数据不一致 |
| Write-Through | 写操作同时写入缓存和DB(同步) | 强一致性(单机) | 对一致性要求极高,写性能可牺牲 | 写延迟增加,缓存与DB耦合 |
单体架构最佳实践: 推荐优先采用 Cache-Aside + 删除 策略,配合合理的 TTL(如 5~10 分钟)作为兜底。对于关键数据(如库存、余额),可考虑使用 Write-Through 模式,但需评估写性能影响。