缓存一致性四大模式深度解析:从理论到架构实战
缓存一致性是分布式系统的核心挑战。本文将详解 Cache Aside 、Read/Write Through 、Write Behind 三大主流模式,并揭示生产级混合架构的最佳实践。
一、缓存一致性问题的本质
在分布式架构中,缓存与数据库双写 (同时更新缓存和数据库)必然面临时序与原子性挑战:
- 时序问题:先更新缓存还是先更新数据库?中间失败如何处理?
- 原子性问题 :如何保证缓存与数据库最终一致?
- 并发问题:多个线程同时读写导致脏数据?
CAP 权衡 :缓存一致性本质是 可用性(A)与一致性(C) 的取舍。
二、Cache Aside(旁路缓存):最务实的选择
2.1 核心思想
应用程序显式管理缓存 ,缓存不感知数据库,仅作为加速层。
标准流程:
读流程:
DB Cache App DB Cache App alt [命中] [未命中] get(key) 返回数据 查询数据库 返回数据 set(key, value) 确认
写流程:
Cache DB App Cache DB App 更新数据 确认 delete(key) // **关键:删除而非更新** 确认
2.2 Java 实现:防击穿优化版
java
@Service
public class CacheAsideService {
@Autowired
private JedisCluster jedisCluster;
@Autowired
private ProductMapper productMapper;
@Autowired
private RedissonClient redissonClient;
/**
* 读取产品信息(带互斥锁防击穿)
*/
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查缓存
String json = jedisCluster.get(cacheKey);
if (json != null) {
// 命中:直接返回(即使此时数据已过期,也返回旧值,保证可用性)
return json.equals("NULL") ? null : JSON.parseObject(json, Product.class);
}
// 2. 未命中:尝试获取分布式锁(防击穿)
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 3. 竞争锁(只有一个线程能进入)
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
// 4. 二次查缓存(可能已加载)
json = jedisCluster.get(cacheKey);
if (json != null) {
return json.equals("NULL") ? null : JSON.parseObject(json, Product.class);
}
// 5. 查数据库
Product product = productMapper.selectById(productId);
// 6. 回填缓存(NULL 值也缓存,防穿透)
if (product != null) {
jedisCluster.setex(cacheKey, 3600, JSON.toJSONString(product));
} else {
jedisCluster.setex(cacheKey, 300, "NULL"); // 空值缓存 5 分钟
}
return product;
}
} catch (Exception e) {
log.error("查询商品失败", e);
} finally {
lock.unlock();
}
// 7. 降级:获取锁失败,直接查数据库(牺牲部分性能保证可用性)
return productMapper.selectById(productId);
}
/**
* 更新产品信息(延迟双删保证一致性)
*/
@Transactional
public void updateProduct(Product product) {
Long productId = product.getId();
String cacheKey = "product:" + productId;
String lockKey = "lock:product:update:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 1. 获取分布式锁
lock.lock(5, TimeUnit.SECONDS);
// 2. 第一次删除(删除旧缓存)
jedisCluster.del(cacheKey);
// 3. 更新数据库(事务保证原子性)
productMapper.update(product);
// 4. 延迟删除(防并发脏数据)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
jedisCluster.del(cacheKey);
// 同时失效本地缓存(如果有)
localCache.invalidate(productId);
}, 500, TimeUnit.MILLISECONDS);
} finally {
lock.unlock();
}
}
}
2.3 优劣分析
| 优点 | 缺点 |
|---|---|
| ✅ 实现简单:逻辑清晰,易于理解和维护 | ❌ 代码侵入:业务代码需显式操作缓存 |
| ✅ 性能高:读操作 99% 命中缓存 | ❌ 一致性问题:写后删除可能失败 |
| ✅ 灵活性强:可自由控制缓存粒度 | ❌ 重试逻辑复杂:需处理删除失败场景 |
| ✅ 适用面广:读多写少场景首选 | ❌ 击穿风险:需额外加锁防护 |
适用场景 :读多写少(读:写 > 10:1),如商品详情、用户画像
三、Read/Write Through(读写透传):缓存驱动
3.1 核心思想
缓存层封装数据库操作,应用只与缓存交互,缓存负责与数据库同步。
架构图:
Cache Through 模式
只与缓存交互
自动回源
应用程序
Cache Layer
Database
3.2 Read Through 流程
缓存未命中时,Cache 层自动加载数据:
java
// 使用 Caffeine 的 CacheLoader 实现
LoadingCache<Long, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build((CacheLoader<Long, Product>) productId -> {
// **自动回源**:缓存未命中时调用此方法
System.out.println("Cache miss, loading from DB: " + productId);
return productMapper.selectById(productId);
});
// 应用层代码(无需感知数据库)
Product product = cache.get(productId); // 未命中自动加载
3.3 Write Through 流程
写操作同时更新缓存和数据库,由缓存框架保证原子性:
java
// 自定义 CacheWriter
cache.writer(new CacheWriter<Long, Product>() {
@Override
public void write(Long key, Product product) {
// 同步写入数据库
productMapper.update(product);
}
@Override
public void delete(Long key, Product product, RemovalCause cause) {
// 删除时同步数据库
productMapper.delete(key);
}
});
// 应用层代码
cache.put(productId, newProduct); // 自动更新数据库
3.4 优劣分析
| 优点 | 缺点 |
|---|---|
| ✅ 业务解耦:应用无需关心数据库 | ❌ 实现复杂:需自定义 CacheLoader/Writer |
| ✅ 强一致性:缓存层保证双写原子性 | ❌ 性能开销:每次写操作需同步数据库 |
| ✅ 简化代码:CRUD 操作统一 | ❌ 灵活性差:难以适应复杂业务逻辑 |
| ✅ 适合框架:Spring Cache 无缝集成 | ❌ 缓存污染:所有数据都进缓存 |
适用场景 :写后立即读(如用户配置更新),或缓存框架能力强的场景
四、Write Behind(异步写回):性能极致
4.1 核心思想
写操作只更新缓存 ,异步批量刷新到数据库,类似操作系统Page Cache机制
流程图:
Queue DB Cache App Queue DB Cache App 后台线程批量消费 loop [每 5 秒 或 1000 条] put(key, value) 立即返回(不等待 DB) 写入变更日志队列 确认 batchUpdate(values) 确认
4.2 Java 实现:批量刷盘
java
@Service
public class WriteBehindService {
private final ConcurrentHashMap<Long, Product> writeBuffer = new ConcurrentHashMap<>();
@Autowired
private JedisCluster jedisCluster;
@Autowired
private ProductMapper productMapper;
/**
* 写操作:只更新缓存,放入缓冲区
*/
public void updateProduct(Product product) {
// 1. 更新缓存(立即生效)
String key = "product:" + product.getId();
jedisCluster.setex(key, 3600, JSON.toJSONString(product));
// 2. 放入写缓冲区(延迟落库)
writeBuffer.put(product.getId(), product);
}
/**
* 定时批量刷盘:每 10 秒或缓冲区满 1000 条触发
*/
@Scheduled(fixedRate = 10000)
public void batchFlushToDB() {
if (writeBuffer.isEmpty()) return;
// 1. 复制缓冲区(避免阻塞)
Map<Long, Product> batch = new HashMap<>(writeBuffer);
writeBuffer.clear();
// 2. 批量更新数据库
try {
productMapper.batchUpdate(new ArrayList<>(batch.values()));
log.info("批量刷盘 {} 条数据", batch.size());
} catch (Exception e) {
// 3. 失败回滚:重新放入缓冲区(带重试次数)
batch.forEach((id, product) -> {
if (product.getRetryCount() < 3) {
writeBuffer.put(id, product);
}
});
log.error("批量刷盘失败", e);
}
}
/**
* 事务保障:JVM 关闭时强制刷盘(防止数据丢失)
*/
@PreDestroy
public void flushOnShutdown() {
if (!writeBuffer.isEmpty()) {
batchFlushToDB();
}
}
}
4.3 优劣分析
| 优点 | 缺点 |
|---|---|
| ✅ 性能极致:写操作仅内存访问,TPS 提升 10 倍 | ❌ 数据丢失风险:宕机导致未刷盘数据丢失 |
| ✅ 削峰填谷:合并写请求,降低数据库压力 | ❌ 一致性弱:存在缓存与数据库不一致窗口 |
| ✅ 批量优化:合并 SQL,减少数据库连接数 | ❌ 实现复杂:需处理重试、幂等、回滚 |
| ✅ 适合批量:日志、埋点等写密集型场景 | ❌ 不适用于金融:无法接受数据丢失的业务 |
五、混合模式:生产级架构实践
5.1 二八法则:80% Cache Aside + 20% Write Behind
读场景(80%) :商品查询、用户浏览 → Cache Aside
写场景(20%) :访问日志、埋点上报 → Write Behind
java
@Service
public class HybridCacheService {
/**
* 读操作:Cache Aside
*/
public Product getProduct(Long id) {
return cacheAsideService.getProduct(id);
}
/**
* 核心业务写:Cache Aside(强一致)
*/
public void updateProduct(Product product) {
cacheAsideService.updateProduct(product);
}
/**
* 埋点写:Write Behind(高性能)
*/
public void trackUserBehavior(UserBehavior behavior) {
writeBehindService.updateBehavior(behavior); // 异步批量落库
}
}
5.2 多级缓存组合:L1 + L2
java
// L1: 本地 Caffeine(防热点)
private final LoadingCache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(1))
.build(id -> jedisCluster.get("product:" + id));
// L2: Redis Cluster(共享)
public Product getProduct(Long id) {
// 1. L1 本地缓存
String json = localCache.get(id);
if (json != null && !json.equals("NULL")) {
return JSON.parseObject(json, Product.class);
}
// 2. L2 Redis(回源后回填 L1)
json = jedisCluster.get("product:" + id);
if (json != null) {
localCache.put(id, json);
return json.equals("NULL") ? null : JSON.parseObject(json, Product.class);
}
// 3. 数据库(双重回填)
Product product = productMapper.selectById(id);
if (product != null) {
String value = JSON.toJSONString(product);
localCache.put(id, value);
jedisCluster.setex("product:" + id, 3600, value);
} else {
localCache.put(id, "NULL");
jedisCluster.setex("product:" + id, 300, "NULL");
}
return product;
}
六、三大模式全面对比
| 维度 | Cache Aside | Read/Write Through | Write Behind |
|---|---|---|---|
| 一致性 | 最终一致 | 强一致(同步双写) | 弱一致(异步延迟) |
| 性能 | 读快写中 | 读快写慢 | 读写极快 |
| 复杂度 | 低 | 中(需框架支持) | 高(需处理重试、幂等) |
| 适用读写比 | 读多写少 (>10:1) | 读写均衡 | 写多读少 |
| 典型场景 | 电商详情页、用户中心 | 配置中心、字典数据 | 日志埋点、计数器 |
| 数据安全 | 高(删除失败可重试) | 最高(同步落库) | 低(宕机丢失) |
| 缓存污染 | 低(按需加载) | 高(所有数据进缓存) | 低(按需) |
| 代码侵入性 | 高(需显式操作) | 低(框架封装) | 中(需管理缓冲区) |
七、生产级最佳实践
7.1 模式选择决策树
读:写 > 10:1
读写均衡
是
否
写多读少
是
否
评估业务场景
读写比例?
Cache Aside
是否需要强一致?
Write Through
Cache Aside
是否可接受数据丢失?
Write Behind
Cache Aside
加互斥锁防击穿
框架封装,简化代码
定时批量刷盘 + 事务保障
7.2 一致性保障组合拳
Cache Aside 优化:延迟双删 + 异步重试
java
public void updateProduct(Product product) {
// 1. 第一次删除
deleteCache(productId);
// 2. 更新数据库
updateDB(product);
// 3. 延迟第二次删除(防并发脏数据)
scheduledExecutor.schedule(() -> {
asyncDeleteCache(productId, 3); // 重试 3 次
}, 500, TimeUnit.MILLISECONDS);
}
Write Behind 优化:批量刷盘 + 失败队列
java
// 批量阈值:1000 条或 10 秒
@Scheduled(fixedRate = 10000)
public void flush() {
if (writeBuffer.size() >= 1000 || timeSinceLastFlush() >= 10) {
batchUpdateDB();
}
}
7.3 监控与告警
| 指标 | 阈值 | 说明 |
|---|---|---|
| 命中率 | > 90% | 过低需调整策略或容量 |
| 缓存加载时间 | < 100ms | 加载过慢影响用户体验 |
| 脏数据时长 | < 1s (Write Behind) | 异步延迟过长需告警 |
| 双删失败率 | < 0.1% | 重试 3 次后仍失败需人工介入 |
八、总结:架构师的权衡哲学
| 模式 | 一句话概括 | 适用场景 | 一致性 | 性能 |
|---|---|---|---|---|
| Cache Aside | 业务驱动,按需加载 | 读多写少(90%场景) | 最终一致 | ⭐⭐⭐⭐ |
| Read/Write Through | 缓存封装一切 | 框架能力强,读写均衡 | 强一致 | ⭐⭐⭐ |
| Write Behind | 异步批量,极致性能 | 日志、埋点(可丢数据) | 弱一致 | ⭐⭐⭐⭐⭐ |
终极建议:
- 通用业务 :Cache Aside(灵活、可控)
- 配置/字典 :Write Through(强一致、简化)
- 分析/日志 :Write Behind(性能优先)
- 大型系统 :混合架构(80% Cache Aside + 20% Write Behind)
缓存一致性没有银弹,只有最适合业务场景的权衡。理解每种模式的优劣,才能在架构设计中游刃有余。