4. 缓存一致性难题:最终一致性、强一致性、分布式事务与缓存同步的权衡

在数据库与缓存构成的混合存储架构中,一致性是最核心也最棘手的权衡点。强一致性保证数据实时准确,但会牺牲可用性与吞吐;最终一致性换取高性能,却可能让用户读到过期数据。本章从工程视角拆解四种主流一致性模型及其对应的缓存同步策略,并给出选型决策框架。

4.1 一致性模型对比

模型 核心承诺 典型延迟窗口 适用场景
强一致性 写入后立即对所有读可见 0(同步阻塞) 金融交易、库存扣减、分布式锁
最终一致性 无更新后,副本最终收敛到一致 毫秒~秒级 用户资料、内容Feed、商品详情
读写一致性 写后读自己的写一定可见 会话级别 用户个人中心、订单状态
单调读一致性 不会读到比之前更旧的数据 无回退 分页列表、消息时间线
工程铁律: 强一致性 ≠ 必须使用分布式事务。通过"缓存穿透写 + 数据库主键版本号"可以在单行场景下实现近似强一致,而无需引入XA或TCC。

4.2 缓存同步的三种典型策略

4.2.1 同步双写(强一致倾向)

应用在同一个事务内同时更新数据库和缓存。若缓存写入失败,事务回滚。该策略能保证数据绝对一致,但会显著增加写入延迟,且缓存故障直接导致写入失败。

// 伪代码:同步双写
@Transactional
public void updateUserBalance(Long userId, BigDecimal amount) {
    // 1. 更新数据库
    userBalanceDao.update(userId, amount);
    // 2. 同步更新缓存(若失败则抛异常触发回滚)
    cacheClient.set("balance:" + userId, amount.toString());
}

4.2.2 异步补偿(最终一致倾向)

先更新数据库,再通过消息队列异步删除或更新缓存。允许短暂的不一致窗口,但吞吐量高、对缓存故障容忍度强。

// 伪代码:异步补偿
public void updateProductStock(Long productId, Integer delta) {
    // 1. 更新数据库
    stockDao.updateStock(productId, delta);
    // 2. 发送缓存失效消息
    mqProducer.send(new CacheInvalidateMsg(productId, "stock"));
}

// 消费者
@RabbitListener(queues = "cache.invalidate")
public void handleInvalidate(CacheInvalidateMsg msg) {
    cacheClient.del("stock:" + msg.getProductId());
}

4.2.3 基于Binlog的监听同步

通过Canal等组件监听数据库Binlog变更,异步同步到缓存。对业务代码无侵入,适合异构系统或遗留系统改造。

推荐实践: 对于大多数互联网业务,异步补偿 + 定期全量对账是性价比最高的方案。同步双写仅用于资金、库存等核心链路。

4.3 缓存一致性架构图

下图展示了从强一致到最终一致的四种同步策略及其适用边界:

缓存一致性策略决策架构 数据库 缓存 同步双写 异步补偿(MQ) Binlog监听 Cache Aside 一致性 — 性能权衡 强一致 ← 同步双写 | 异步补偿 | Binlog监听 | Cache Aside → 最终一致 低吞吐 ← ———————————————— → 高吞吐

4.4 分布式事务与缓存同步的冲突

当引入分布式事务(如TCC、Saga)时,缓存同步面临新的挑战:

  • TCC 的 Try-Confirm 阶段:Try阶段预留资源时,缓存应标记为"中间态";Confirm后才能真正更新缓存。若直接更新,Cancel时缓存回滚困难。
  • Saga 的补偿操作:补偿操作需要反向更新缓存,但缓存中可能已混入其他并发写入,导致补偿后的缓存值与数据库不一致。
  • 本地消息表 + 缓存:事务消息投递成功后,消费者更新缓存。若消费者重复消费,需保证缓存操作的幂等性。
避坑提示: 不要试图在分布式事务内部同步操作缓存。事务的ACID与缓存的最终一致性本质冲突。正确做法是:事务只负责数据库状态,缓存通过异步事件或Binlog独立同步。

4.5 选型决策矩阵

业务场景 推荐策略 一致性等级 容忍延迟
账户余额、库存扣减 同步双写 + 本地事务 强一致 < 10ms
订单状态流转 异步补偿 + 会话一致性 读写一致 < 500ms
商品详情、用户资料 Cache Aside + 定期对账 最终一致 秒级
搜索索引、推荐特征 Binlog监听 + 批量刷新 最终一致 分钟级

4.6 一致性保障的工程手段

  1. 版本号乐观锁:缓存中存储数据版本号,更新时对比版本,防止旧数据覆盖新数据。
  2. 延迟双删:先删除缓存,再更新数据库,延迟几百毫秒后再删一次缓存,解决并发读请求导致的脏缓存。
  3. 缓存失效时间兜底:所有缓存设置合理的TTL,即使同步失败,也能在TTL过期后自动恢复一致。
  4. 全量对账任务:定期扫描数据库与缓存,发现不一致后自动修复,作为最终防线。
核心结论: 缓存一致性没有银弹。强一致用同步双写 + 事务,最终一致用异步补偿 + 对账。关键在于识别业务对一致性的真实需求,避免为"理论上的强一致"付出过高的性能代价。