架构师必备:缓存更新模式总结

大家好,我是Java烘焙师。如何更新缓存和DB、做到性能和一致性的取舍,是一个很常见的话题。下面结合笔者的经验和思考,系统性地总结一下缓存更新模式,讲透讲明白。

1、旁路缓存(cache-aside)

实现方案

  • 查询:先查缓存,查不到缓存时再查DB,并把DB内容写入缓存、设置合适的过期时间
  • 更新:先更新DB,再删缓存;做到极致则需引入延迟双删机制

之所以不是先删缓存、再更新DB,是因为在这两个操作间隙,如果有其它查询请求,则会把DB旧值写到缓存。

之所以不是先更新DB、再更新缓存,是因为写DB和缓存无法保证一致性,并且可能因为2个并发写的时序问题而把旧数据写到缓存。

之所以延迟双删,是因为在极端情况下,读线程会把DB旧值写到缓存。需要同时满足几个条件:缓存已过期,并且读线程先查询到DB旧值,然后写线程更新DB、删除缓存之后,读线程才把DB旧值写入缓存。如下图所示。

因此第一次删除缓存后,延迟一小段时间再删除,就能保证缓存和DB的最终一致。下图是引入了延迟双删机制的cache-aside架构图。

cache-aside查询场景:

cache-aside更新场景:

适用场景

  • 绝大部分场景

优点

  • 当数据量大时,可按需加载到缓存

缺点

  • 如果存在热点key,在失效后,会有大量查询请求穿透缓存,直接打到DB,造成DB CPU使用率飙升

旁路缓存优化:主动预刷新缓存

为了解决热点缓存失效问题,可考虑设置TTL为较长时间,并主动预刷新热点key。

根据数据量大小区分:

  • 如果数据量较大,则针对热点key,配置白名单。做得更好的话,是自动发现、并更新热点key白名单。
  • 如果数据量较小,则可以考虑全部加载到缓存中,永不过期。如:一些全局的配置数据。

根据触发刷新缓存的时机区分:

  • 定时拉取:程序自行实现,根据热点key白名单,定时查DB、并更新缓存
  • 异构数据:监听mysql变更,DB变化时触发更新缓存

更推荐异构数据的方式,好处是:缓存更新及时,并且做成通用功能之后、无需额外开发。

2、异步写回DB模式(write-back)

实现方案

  • 查询:只查询缓存
  • 更新:先写入缓存,然后发消息、消息链路异步写入DB,或定时任务兜底写入DB

适用场景

查询qps很高、极其热点的数据,优先保证性能。

场景举例:

  • 计数统计:有的页面会滚动刷新访问人次、使用人次
  • 爆品库存扣减:redis扣减库存,然后异步落库,而不是常规地操作DB扣减库存

优点

  • 支撑高qps、热点场景

缺点

  • 短期内会出现缓存和DB数据不一致情况,需要消息触发、或兜底定时任务写回DB

3、read/write through模式

实现方案

不论是cache-aside、还是write-back模式,都需要应用程序自己来控制读写缓存、DB。而read/write through模式是把控制权交给底层存储服务。

存储服务维护缓存、持久化数据,应用程序无需感知,这也是优点了。不过完全依赖于存储服务是否靠谱,实际业务场景并不常见。

4、持续优化

搭积木方式,根据实际情况做优化。

多级缓存:进一步降低缓存、DB的热点风险

  • 增加本地缓存,如caffeine
  • 或增加DB以外的异构数据,当查不到缓存时再查异构数据、查不到异构数据时最终查DB。异构数据可以是HBase、ES等

通过逻辑层面来实现生效、过期的效果,而非系统层面

  • 架构设计必须适配业务,比如通过逻辑过期解决不一致、缓存集中过期的问题,如缓存记录业务开始时间、结束时间,TTL可设置稍长些、并且通过增加随机时长来避免key集中失效。这样就能实现到时间点就变的场景,如活动开始、结束。

强一致场景,只查DB、已DB数据为准

  • 特别地,对一致性有强要求的场景:只查DB、不查缓存,以DB数据为准。如下单时查询DB里的价格,避免缓存数据非最新。

更进一步,考虑使用rocksdb,代替redis

  • rocksdb相当于是自带缓存的持久化数据库,值得专门写一篇文章介绍原理、区别,后面有空整理。

结论

  • 绝大部分场景,使用旁路缓存模式(cache-aside)。更进一步,对部分热点key做主动预刷新,可监听DB变更、或定时刷新。
  • 高qps、极热key场景,使用异步写回DB模式(write-back),优先保证性能,可接受短时间内DB与缓存不一致。
  • 持续优化:
    • 增加多级缓存、异构数据,来降低缓存、DB的热点风险
    • 通过逻辑层面来实现生效、过期的效果
    • 强一致场景,只查DB、已DB数据为准

延伸阅读:笔者之前写的缓存相关文章,欢迎围观。

相关推荐
马走日mazouri2 小时前
深入理解MySQL主从架构中的Seconds_Behind_Master指标
数据库·分布式·mysql·系统架构·数据库架构
无敌的神原秋人11 小时前
关于Redis不同序列化压缩性能的对比
java·redis·缓存
秋难降13 小时前
零基础学习SQL(十一):SQL 索引结构|从 B+Tree 到 Hash,面试常问的 “为啥选 B+Tree” 有答案了
数据库·后端·mysql
ljh57464911914 小时前
mysql 必须在逗号分隔字符串和JSON字段之间二选一,怎么选
数据库·mysql·json
百思可瑞教育14 小时前
Vue中使用keep-alive实现页面前进刷新、后退缓存的完整方案
前端·javascript·vue.js·缓存·uni-app·北京百思可瑞教育
论迹14 小时前
【Redis】-- 持久化
数据库·redis·缓存
gamers15 小时前
rock linux 9 安装mysql 5.7.44
linux·mysql·adb
努力的小郑16 小时前
MySQL索引(四):深入剖析索引失效的原因与优化方案
后端·mysql·性能优化
江团1io017 小时前
深入解析MVCC:多版本并发控制的原理与实现
java·经验分享·mysql