4. 缓存一致性核心挑战:并发写冲突、数据源与缓存双写不一致、最终一致性与强一致性取舍

缓存一致性是分布式系统中最棘手的难题之一。当数据同时存在于缓存(Redis)与持久化存储(MySQL)中时,任何写操作都可能打破两者之间的同步状态。本章深入剖析三大核心挑战:并发写冲突、双写不一致以及一致性模型的选择,并给出可落地的治理策略。

核心观点:缓存一致性没有银弹。业务对一致性的容忍度决定了技术方案的选择。金融交易要求强一致性,而内容推荐系统可以接受最终一致性。

4.1 并发写冲突:当多个线程同时修改同一份数据

并发写冲突发生在多个客户端或服务实例同时对同一缓存键(Key)进行写操作时。典型场景:用户A和用户B几乎同时修改同一条订单的物流地址。

冲突类型 触发条件 典型后果
缓存后写覆盖 线程1写缓存,线程2写缓存,线程1写DB,线程2写DB 缓存与DB最终状态不一致,数据丢失
缓存与DB交叉写 线程1写DB,线程2写缓存,线程1写缓存,线程2写DB 缓存与DB永久错位,难以恢复
删除缓存与更新DB时序错乱 线程1删除缓存,线程2读缓存未命中并回填旧数据,线程1更新DB 缓存中残留旧数据,DB已更新

解决并发写冲突的核心思路是串行化或版本控制:

  • 分布式锁:对同一业务Key加写锁,确保同一时刻只有一个线程能执行写操作。
  • 乐观锁(CAS):利用Redis的WATCH命令或MySQL的版本号字段,写操作前校验数据版本。
  • 消息队列串行化:将写请求放入有序队列,由单一消费者顺序处理。
实战建议:对于高并发写入场景,优先使用分布式锁 + 缓存删除策略。锁的粒度要细到业务主键,避免锁住整个表或整个缓存分区。
并发写冲突时序图(缓存后写覆盖) 线程1 线程2 Redis缓存 MySQL 写 SET key v1 写 SET key v2 写 UPDATE t SET val=v1 写 UPDATE t SET val=v2 结果:缓存 = v2,DB = v2(看似一致,但线程1的v1丢失) 若线程1先写DB后写缓存,则缓存 = v1,DB = v2,永久不一致

4.2 数据源与缓存双写不一致:四种经典模式

双写不一致是指应用程序同时更新缓存和数据库时,由于操作顺序、网络延迟或部分失败导致两者数据不同步。以下是四种经典模式及其治理策略:

模式 操作顺序 不一致风险 推荐治理方案
先写DB,后写缓存 DB成功 → 缓存写入失败 缓存为旧数据,DB为新数据 重试机制 + 异步补偿
先写缓存,后写DB 缓存成功 → DB写入失败 缓存为新数据,DB为旧数据(脏数据) 事务回滚 + 缓存删除
先删缓存,后写DB 缓存删除成功 → DB写入前有并发读 并发读回填旧数据到缓存 延迟双删(Double Delete)
先写DB,后删缓存 DB成功 → 缓存删除失败 缓存残留旧数据 异步删除 + 消息队列保障
避坑指南:“先删缓存,后写DB”模式在并发读场景下几乎必然出现不一致。单纯依赖“缓存过期”来修复,在过期窗口期内用户会读到脏数据。务必配合延迟双删或版本号校验。

4.3 最终一致性与强一致性取舍

一致性模型的选择直接决定了系统架构的复杂度与吞吐能力。没有一种模型能同时满足所有业务场景。

  • 强一致性:每次读操作都能读到最新的写操作结果。实现代价最高,通常需要分布式事务(如2PC、TCC)或共识算法(如Paxos、Raft)。适用于金融交易、库存扣减等场景。
  • 最终一致性:系统保证在没有新写操作后,所有读操作最终能读到最新值。允许中间状态不一致。实现成本低,吞吐量高。适用于社交动态、内容缓存等场景。
  • 读写一致性(Read-Your-Writes):用户写后立即读,保证读到自己的写入。对其他用户的读不保证。适用于用户个人设置、购物车等场景。
一致性模型 延迟 吞吐量 实现复杂度 典型业务
强一致性 高 低 极高 支付、库存、账户余额
最终一致性 低 高 低 新闻Feed、推荐列表、计数
读写一致性 中 中 中 用户资料、订单状态
架构决策原则:优先满足业务对一致性的最低要求。能用最终一致性解决的,绝不引入强一致性带来的性能损失。对于核心链路,采用“强一致性兜底 + 最终一致性加速”的混合策略。
一致性模型取舍决策树 业务一致性需求 必须强一致? 可接受最终一致? 分布式事务 / 共识算法 适用:支付、库存、账户 异步补偿 / 缓存删除 适用:内容、推荐、计数 混合策略:核心链路强一致 + 非核心链路最终一致 例如:下单扣库存用分布式锁 + 事务,商品详情页用缓存最终一致

4.4 实战案例:订单状态更新中的一致性治理

以电商订单状态更新为例,展示如何综合应对上述挑战:

  1. 业务场景:用户支付成功后,订单状态从“待支付”变为“已支付”。需要同步更新MySQL订单表和Redis缓存。
  2. 并发风险:支付回调可能重复调用,用户可能同时取消订单,导致状态冲突。
  3. 治理方案:
    • 使用Redis分布式锁(Key = order:lock:{orderId})防止并发写。
    • 采用先写DB,后删缓存模式,配合异步延迟双删(第一次删除在DB写入后立即执行,第二次在1秒后执行)。
    • DB层使用乐观锁(版本号字段),更新时校验当前状态是否允许变更。
    • 缓存删除失败时,通过消息队列进行重试,最多重试3次。
// 伪代码:订单状态更新
public boolean updateOrderStatus(String orderId, String newStatus) {
    String lockKey = "order:lock:" + orderId;
    String lockValue = UUID.randomUUID().toString();
    boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS);
    if (!locked) {
        return false; // 获取锁失败,重试或返回
    }
    try {
        // 1. 先更新DB(带版本号乐观锁)
        int rows = orderMapper.updateStatusWithVersion(orderId, newStatus, currentVersion);
        if (rows == 0) {
            return false; // 版本冲突,更新失败
        }
        // 2. 立即删除缓存
        redisTemplate.delete("order:" + orderId);
        // 3. 异步延迟双删(1秒后再次删除)
        delayedDeleteCache("order:" + orderId, 1000);
        return true;
    } finally {
        // 释放锁
        redisTemplate.delete(lockKey);
    }
}
关键指标:治理后,订单状态不一致率从0.5%降至0.001%以下,接口P99延迟从120ms降至85ms(得益于缓存删除代替缓存写入,减少了一次网络IO)。

4.5 本章小结

缓存一致性治理的核心在于:

  • 识别冲突类型:并发写冲突、双写顺序不一致、读写并发导致的数据回填。
  • 选择合适模式:“先写DB后删缓存”是当前业界最推荐的通用模式,配合延迟双删和异步重试。
  • 明确一致性边界:与业务方对齐一致性要求,避免过度设计。强一致性只用于核心资金链路。
  • 工具链保障:分布式锁、乐观锁、消息队列、版本号是治理工具箱中的必备武器。