并发控制与缓存一致性技术选型之权衡术
核心权衡点:锁的量级与性能损耗的权衡 选择哪种并发控制或一致性手段,本质是在 一致性强度 与 系统吞吐 / 延迟 之间做交易。不存在普适最优解,只有基于回源成本、并发量级和跨实例协调代价的按需组合。 量级轻 → 延迟低,但保护弱:进程内 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 兜底。 记住:锁是最后的手段,不是默认选项。 能靠最终一致和降级解决的,不要用强同步去惩罚高并发。