核心权衡点:锁的量级与性能损耗的权衡
选择哪种并发控制或一致性手段,本质是在 一致性强度 与 系统吞吐 / 延迟 之间做交易。不存在普适最优解,只有基于回源成本、并发量级和跨实例协调代价的按需组合。
- 量级轻 → 延迟低,但保护弱:进程内 singleflight,非阻塞软标记,纯 TTL 过期。
- 量级重 → 一致性强,但代价高:跨实例分布式锁,强同步写穿透。
下面从“数据流”与“控制面”两个维度梳理关键技术,并给出组合原则。
1. 数据一致性模式(如何同步缓存与数据库)
这些模式关注当源数据变更时,缓存如何更新或失效,不直接涉及并发控制,但影响选型。
Cache Aside(旁路缓存)
- 模式:读未命中则查 DB 并回写缓存;写直接更新 DB,然后删除缓存。
- 锁量级:写操作无锁,仅单一 DEL;读可配合 singleflight 防击穿。
- 权衡:最终一致窗口 = 删缓存到下次重建之间;删除失败需重试或 TTL 兜底。
- 适用:读多写少,可接受短暂不一致的场景(大多数互联网业务)。
Read/Write Through(读写穿透)
- 模式:缓存层代理所有 DB 读写,业务只与缓存交互。
- 锁量级:同步写,需保证缓存与 DB 的原子更新,往往引入分布式锁或事务消息。
- 权衡:一致性强,但每次写都要同时操作缓存+DB,写入延迟高,实现复杂。
Write Behind(异步回写)
- 模式:写只更新缓存,异步批量刷回 DB。
- 锁量级:缓存写入轻量,但需要队列/日志保证不丢数据。
- 权衡:写入性能极高,一致性很弱,允许丢数据窗口。
延迟双删与订阅刷新
- 延迟双删:写 DB 前先删缓存,DB 更新完成后延迟再删一次(用于规避主从延迟导致的不一致)。
- Binlog 订阅(如 Canal):异步监听 DB 变更流水,精确删除或更新缓存。
- 锁量级:均为异步,无额外锁竞争,但引入消息延迟和架构复杂度。
本系统选择:Cache Aside + MQ 异步删除,利用已有死信队列保障最终一致性,兼顾简单和可靠。
2. 并发控制机制(如何防击穿、防并发重建)
singleflight(进程内请求合并)
- 量级:极轻,无网络开销,仅内存 map + 阻塞等待。
- 保护范围:单进程内相同请求的合并。
- 性能损耗:少数请求等待首次执行完毕,延迟可忽略。
- 适用:高并发下同一批缓存键临时穿透 Redis 的场景。
分布式锁(如 Redis SET NX EX)
- 量级:重,涉及网络往返、轮询等待、锁 TTL 管理和 Lua 释放。
- 保护范围:跨实例互斥,确保单点重建。
- 性能损耗:锁竞争导致额外延迟(轮询 100ms+),可能成为瓶颈。
- 适用:回源成本极其高昂且要求强一致的场景(如复杂报表、详情缓存重建)。
软标记 / SETNX 提示(非阻塞跨实例通知)
- 量级:极轻,一次 Redis SETNX 无等待。
- 保护范围:跨实例“知情权”,让其他实例主动降级而非常规等待。
- 性能损耗:基本无延迟,仅需判断标记存在与否。
- 适用:重建允许降级或短暂不一致的场景(批量读、冷缓存预热)。
仅靠 TTL
- 量级:无。
- 保护:无任何并发控制。
- 损耗:零,但可能发生缓存击穿或雪崩。
- 适用:数据可大量容忍陈旧,或变更极低频。
3. 组合决策矩阵
| 场景 | 回源成本 | 推荐组合 | 理由 |
|---|---|---|---|
| 视频实体批量读取(高频、可降级) | 中(批量主键查询) | singleflight + 软标记 | 进程内去重 + 跨实例非阻塞防多余穿透 |
| 视频详情页读取(中频、不期望旧数据) | 中高(复杂 SQL 或关联查询) | 分布式锁 + double-check | 强控单实例重建,避免多实例重复计算 |
| 关注流冷缓存重建(低频、允许最终一致) | 高(多表聚合排序) | 软标记 + 单一执行 | 不强制等待,接受少量重复重建成本优于锁等待 |
| 写操作后缓存失效(Cache Aside) | 极低(DEL 命令) | 无需锁,直接 DEL | 并发写由 DB 串行化,缓存仅打扫战场 |
4. 一句话选型指南
- 先上 singleflight,解决绝大多数进程内并发穿透。
- 多实例部署且重建轻量时,补 软标记 提示降级,避免引入锁。
- 只有当“多个实例并发重建会造成无法接受的成本或数据矛盾”时,再引入 分布式锁。
- 缓存更新一律走 Cache Aside 异步删除,放弃复杂同步,靠重试和 TTL 兜底。
记住:锁是最后的手段,不是默认选项。 能靠最终一致和降级解决的,不要用强同步去惩罚高并发。