分布式缓存更新没有银弹,最稳妥的方案是Cache Aside模式配合延迟双删,但具体选型必须看业务对一致性和吞吐量的容忍度。
缓存更新策略怎么选:先看业务场景再谈技术
很多团队在缓存更新上栽跟头,不是因为不懂技术,而是上来就纠结“哪个策略最好”。没有最好的策略,只有最合适的场景,电商秒杀、用户会话、商品详情、配置中心,对一致性的要求天差地别。
先删缓存还是先更新数据库:经典问题的答案
这是分布式缓存领域被讨论最多的问题,先更新数据库再删缓存,和先删缓存再更新数据库,各有各的坑。
先删缓存再更新数据库,在并发读写下容易出问题,一个读请求在缓存删除后、数据库更新前到来,会把旧数据写回缓存,导致后续请求一直读到脏数据,这个问题在读写比例高的场景下会被放大。
先更新数据库再删缓存,理论上更安全,因为缓存删除失败的概率低于并发写回的概率,但依然存在一个时间窗口:更新数据库和删除缓存之间,读请求可能命中旧缓存。延迟双删能缓解这个问题更新数据库后先删一次缓存,等待几百毫秒,再删一次,兜底掉并发写回的数据。
行业共识认为,绝大多数业务场景下,先更新数据库再删缓存,配合延迟双删,是性价比最高的方案,它不需要引入额外组件,代码改动小,对现有架构侵入性低。
三大主流缓存更新策略对比:Cache Aside、Write Through、Write Back
除了上面说的延迟双删,行业内还有几种成熟的策略模式,理解它们的差异,才能在做技术选型时心里有底。
Cache Aside模式:应用层控制的经典方案
这是最常用的策略,读请求先查缓存,没命中就查数据库,然后回填缓存,写请求先更新数据库,再删除缓存,Cache Aside把缓存和数据库的操作解耦,应用层完全掌控流程,灵活度高。
缺点是逻辑散落在业务代码里,每个需要缓存的地方都要写一遍,先更新DB再删缓存”在极端并发下仍可能短暂不一致,延迟双删就是对这个缺陷的补充。
Write Through模式:缓存为主导的强一致方案
Write Through要求写请求只写缓存,由缓存组件同步写数据库,只需要跟缓存打交道,数据库的写入由缓存层代理完成,这种模式保证了缓存和数据库的强一致,因为写操作是同步串行的。
代价是写延迟变高,因为每次写都要等数据库确认,一旦缓存组件出问题,所有写请求直接失败。Write Through适合对一致性要求极高、写并发不高的场景,比如账户余额变更、订单状态流转。
Write Back模式:极致性能下的最终一致方案
Write Back同样只写缓存,但缓存不会同步写数据库,而是异步批量刷盘,写入性能极高,因为只操作内存,但宕机风险不容忽视缓存里的数据还没落库,机器一挂就丢了。
Write Back适合允许数据丢失、对写吞吐要求极高的场景,比如用户浏览记录、点赞计数、埋点日志。电商高并发场景缓存更新策略怎么选,如果涉及库存扣减,千万别用Write Back,扣减数据丢了可是要出大事的。
| 策略模式 | 一致性 | 写性能 | 实现复杂度 | 典型场景 |
|---|---|---|---|---|
| Cache Aside | 最终一致(可优化) | 中 | 低 | 商品详情、用户信息 |
| Write Through | 强一致 | 低 | 中 | 账户、订单、配置 |
| Write Back | 最终一致(有丢失风险) | 高 | 高 | 日志、计数、浏览记录 |
缓存更新失败怎么办:消息队列兜底与重试机制
策略选好了,但执行时总会有意外,缓存删除失败、数据库更新超时,这些异常情况怎么处理,直接决定了系统的最终一致性水平。
基于Binlog的异步删除方案
监听MySQL的Binlog变更,解析出数据变更事件,异步发送到消息队列,再由消费者执行缓存删除操作,这个方案把缓存更新和业务主流程完全解耦,业务代码里不需要写任何缓存删除逻辑。
好处很明显:主流程只操作数据库,缓存更新天然异步化,即使缓存组件抖动,也不会影响业务主链路,而且Binlog方案能精确感知数据变更,不需要人工维护缓存key列表。
消息队列加定时任务的重试机制
如果使用延迟双删,每次删除操作都发送到消息队列,消费者收到后执行删除,一旦删除失败,消息会进入重试队列,按指数退避策略重试,定时任务扫描一定时间窗口内未成功删除的缓存key,做最终兜底。
这套机制能处理绝大多数缓存删除失败场景,但要注意消息队列本身的高可用,消息队列挂了,整个兜底链路就断了,所以生产环境必须给消息队列做主从部署,并设置合理的重试次数上限,避免消息堆积。
缓存与数据库双写一致性的深度实践:从理论到落地
前面聊的都是策略和模式,具体落地时还有很多细节决定成败,这里直接给出可操作的步骤和命令。
操作路径:从Redis管理到数据一致性保障
实际工作中,缓存更新的操作路径是有约定俗成的套路的,以最常见的Cache Aside加延迟双删为例:
- 更新数据库:执行UPDATE语句,记录变更时间戳。
- 删除缓存:第一次删除对应的Redis缓存key。
- 延迟等待:根据业务耗时,等待300-500毫秒(可通过Redis的
SETEX命令实现可配置延迟)。 - 再次删除:执行第二次删除,兜底并发写回。
- 记录审计:在日志中记录缓存key、操作时间、删除结果,便于排查问题。
这套流程需要配合Redis的监控命令使用。INFO命令查看缓存命中率,SLOWLOG GET查看慢查询,MONITOR命令实时跟踪Redis执行的每一条命令,这些命令能帮你快速定位是缓存删慢了,还是数据库更新慢了。
版本号机制:让缓存数据自带时间戳
延迟双删不是万能的,极端情况下两次删除之间仍有并发写回,更严谨的做法是给缓存数据加版本号,每次更新数据库时,版本号加一,写入缓存时,把版本号一起存进去,读请求发现缓存的版本号小于数据库当前版本号,立即丢弃缓存内容,回源数据库。
这个方案在架构上更干净,但需要业务层配合维护版本号字段。如果数据库里已经有updated_at这样的时间戳字段,可以直接复用,减去额外维护成本。
缓存过期时间设置:最后的兜底防线
无论用什么更新策略,缓存过期时间都必须设置,它是数据最终一致性的最后一道防线,即使前面的机制全部失效,缓存过期后读请求也会强制回源数据库,拉回最新数据。
过期时间设置没有统一标准,但有几个参考原则:
- 热点数据:过期时间可设置在30分钟到2小时之间,兼顾命中率和一致性。
- 核心交易数据:过期时间缩短到5-10分钟,即使更新失败,数据也能快速恢复一致。
- 非核心展示数据:过期时间可延长到24小时,减少数据库压力。
需要注意,过期时间设置过短会导致缓存频繁失效,数据库压力陡增;设置过长则脏数据存活时间太久,建议结合业务监控数据,动态调整过期策略。
常见问题解答:缓存更新策略的实践困惑
缓存更新策略用哪种最简单可靠?
对于大多数中小团队,先更新数据库再删缓存,配合延迟双删和消息队列兜底,是最简单可靠的方案,它不需要引入复杂组件,逻辑直观,排查问题也容易,如果团队对一致性要求极高,且写并发不大,可以考虑Write Through模式,但要做好性能损耗的准备。
为什么缓存删了还是读到旧数据?
这个问题通常有三个原因:一是删除操作本身失败,比如网络抖动或Redis超时,消息队列兜底没生效;二是删除前有并发读请求把旧数据写回缓存,延迟双删的时间窗口没覆盖到;三是缓存key设计有误,业务代码里新旧key不一致,排查时先用MONITOR命令看Redis实际接收到的命令序列,再对照业务日志确认操作顺序。
微服务架构下缓存更新有什么额外注意点?
微服务拆分后,同一个数据可能被多个服务读写,此时缓存更新逻辑必须收敛到数据归属方服务中,其他服务通过接口调用,不能直接操作缓存,否则会出现多个服务各自维护缓存,互相覆盖的问题。建议在服务层面统一封装缓存读写组件,所有缓存操作走同一套代码路径,避免逻辑分叉,跨服务的数据变更可通过Binlog方案统一处理,保证所有下游服务感知到同一份数据变更事件。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/558595.html
