第3章 强一致性方案设计:分布式锁 + 双写、写后立即失效、同步刷新机制
当业务对缓存与数据库的一致性要求达到“任何时候读取缓存都必须返回最新已提交数据”时,弱最终一致性方案不再适用。本章围绕分布式锁 + 双写、写后立即失效、同步刷新三种典型强一致手段展开,明确各自的应用边界、组件分层、数据流与控制流,并给出落地步骤与验收标准。
3.1 方案边界
- 适用场景:金融交易、库存扣减、订单状态、账户余额等对数据准确性要求零容忍的业务。
- 不适用的边界:高并发读多写少且允许短暂不一致的查询场景(如资讯列表、用户主页),此时强一致会带来不必要的锁开销与延迟。
- 一致性等级:线性一致性(Linearizability)——所有操作实时可见且顺序一致,等同于单副本数据库的行为。
3.2 分层架构
三个方案共享统一的分层模型,但锁与失效策略的部署位置存在差异:
| 分层 | 组件 | 职责 |
|---|---|---|
| 接入层 | API Gateway / LB | 请求路由、限流、参数校验 |
| 业务服务层 | 写服务 / 读服务 | 执行双写、锁获取、缓存失效与刷新 |
| 分布式协调层 | Redis / Zookeeper / Etcd | 提供分布式锁(互斥、可重入、自动续期) |
| 缓存层 | Redis Cluster / Memcached | 缓存键值存储,支持TTL与显式删除 |
| 持久层 | MySQL / PostgreSQL | 写入业务数据,作为权威数据源 |
组件职责洞察:分布式锁只在双写方案中必须前置获取;写后立即失效无需锁,但需要保证失效操作的原子性;同步刷新方案则依赖锁防止并发刷新。
3.3 数据流 / 控制流 / 信任链路
3.3.1 分布式锁 + 双写
- 控制流:写服务先请求分布式锁 → 获取成功后写入数据库 → 写入缓存(双写) → 释放锁。
- 数据流:业务数据从服务 → DB(持久化) → 缓存(冗余);在读请求未命中缓存时,读服务直接回查DB,但须等待锁释放。
- 信任链路:锁服务担保写操作互斥,DB存储了最终数据,缓存只是热读副本。
3.3.2 写后立即失效
- 控制流:写服务直接更新DB → 随即调用 cache.del(key) 删除缓存 → 业务结束。
- 数据流:后续读请求必然缓存穿透,回查DB并重新加载最新数据写入缓存。
- 信任链路:DB始终是最新,缓存只存在短暂缺失而非不一致。
3.3.3 同步刷新机制
- 控制流:写服务先获取分布式锁 → 更新DB → 同步调用缓存更新(SET) → 释放锁。
- 数据流:缓存数据与DB同时更新,锁保证了串行化写入,避免并发写入脏数据。
- 信任链路:锁 + 原子性更新保证缓存与DB完全同步,但吞吐受限。
信任链路陷阱:任何写后失效或双写方案,若缓存删除操作因网络故障失败,将产生长期不一致。必须配合定期一致性校验或延迟双删作为补偿。
3.4 选型权衡
| 评估维度 | 分布式锁+双写 | 写后立即失效 | 同步刷新 |
|---|---|---|---|
| 一致性强度 | 最强(线性) | 强(基本线性,依赖删除原子性) | 最强(线性) |
| 写吞吐 | 低(锁竞争) | 高(无锁) | 较低(锁+同步写缓存) |
| 读延迟抖动 | 低(缓存命中) | 高(频繁穿透) | 低(缓存始终热) |
| 实现复杂度 | 高(锁续期、防死锁) | 低 | 中 |
| 故障场景风险 | 锁超时导致双写不一致 | 删除失败导致缓存残留旧数据 | 锁失效能引起并发半写 |
推荐选型决策树:
- 能否接受缓存短暂缺失(数百ms)? → 能 → 选写后立即失效
- 写并发低于500ops且要求零数据冲突? → 选分布式锁+双写
- 读请求极频繁且写少,需缓存始终命中? → 选同步刷新(配合长TTL)
3.5 风险点与应对
- 锁死锁 / 锁超时:锁持有时间超长导致服务雪崩。应对:自动续期 + 看门狗(如 Redis Client Redisson 的机制)。
- 双写不一致:当写DB成功但写缓存失败时,缓存为旧数据。应对:重置缓存值并记录失败日志,由补偿Job对账修复。
- 缓存删除失败:写后立即失效场景下,如果删除命令丢失,缓存永久陈旧。应对:延迟双删(先删缓存 → 写DB → 再删缓存),或引入版本号实现CAS。
- 缓存雪崩:大量缓存在同一时刻失效(写后失效模式下)。应对:随机TTL或本地缓存降级。
3.6 落地步骤
- 识别热点键:通过监控按QPS排序,筛选需要强一致保障的缓存键。
- 引入分布式锁 SDK:选型 Redis Redlock / Etcd watch + lease,提供统一锁接口。
- 改造写服务:在业务事务单元内嵌入双写或刷新逻辑,确保锁范围包裹全部写入动作。
- 设计缓存失效补偿:对每次删除操作记录日志(如 DB 的
cache_invalid_log表),离线Worker每分钟扫描未确认的删除并重试。 - 灰度上线:先对5%流量开启强一致方案,对比数据差异指标。
- 压测验证:使用并发写入脚本模拟竞态,验证最终数据是否线性一致。
3.7 验收标准
| 验收项 | 校验方法 | 通过条件 |
|---|---|---|
| 写后立即读取一致性 | 并发写入程序,随后立刻读缓存 | 每次读到的值等于最近一次成功写入的值(误差 < 5ms) |
| 缓存空洞时间 | 模拟单次写,记录缓存删除后到下次回填完成的时间 | P99 空洞时间 < 200ms |
| 分布式锁死锁逃逸 | 强制中断持有锁的线程后,等待锁超时自动释放 | 锁在10s内自动释放,后续请求正常执行 |
| 补偿修复率 | 人为注入缓存删除失败日志,观察补偿Job修复情况 | 补偿覆盖率 ≥ 99.99%,修复延迟 < 1min |
验收工具:建议使用
jepsen 或自定义 go-consistency-test 执行线性一致性检验,导出日志作为验收依据。