缓存更新策略小结
tips:这里讲的缓存是 redis 等组件对 db 数据的缓存,非计算机中的 L1、L2 的高速缓存
这里主要介绍同步更新的三个模式+一个异步更新模式,讨论各个模式固有的问题,以及更新失败、数据库主从条件下的各个模式的问题。
同步更新1 Cache Aside 模式
由程序主动管理cache、db
read 流程:程序首先从 cache 中读取数据,a.命中则返回 b.未命中则主动从 db 读取并更新到 cache 中,再返回
write 流程:根据先操作 db 还是先操作 cache,以及对 cache 主动更新还是单纯失效可以分很多种
write 流程 1:先update db,再update cache
问题:对于两个并发对的写操作可能导致脏数据
参考以下时序示意图:
update1----->写A到db ----------------------------------->更新 cache 为A
update2---------------->写B到db-------->更新 cache 为B
触发条件:对并发写操作,后更新的操作如果有一定延时的波动,可能会导致最新 db 数据为 B,但缓存的数据为 A
write 流程 2:先delete cache,再 update db
问题:并发的读写可能导致脏数据
update------>删除 cache 内容A--------------------------->更新 db 为 B
read-------------->读 DB 内容为 A----->更新 cache 为 A
触发条件:对于并发读写,删除缓存后续的读操作可能在写入 db 之前更新了缓存,导致最新 db 数据为 B,但缓存的数据为 A
write 流程 3:先update db,再delete cache
同流程 2 相比,后续对cache 从主动更新变为了删除,后续的更新主要依靠后续的 read 流量触发
但依然可能有问题:
read----> cache未命中--->读 db 为 A------------------------------------>更新 cache为 A
update--------------------------------->更新db 为 b----->删除cache
触发条件:1、update 时有缓存未命中的 read 操作更新 cache。2、更新 db 要晚于读db 的操作,且删除 cache 要早于更新 cache 的操作
第一个条件触发概率已经比较小,而第二个条件对db的更新操作耗时通常要远大于对 db 的读操作耗时,两个条件都发生得到概率就更小了。
为了兜底通常对缓存还需要加过期时间。
同步更新2 read/write through 模式
程序只管理 cache,缓存到数据库的同步对程序透明
read through
程序从 cache 中读取数据 a.命中则立即返回 b.未命中则 cache 自动从 db 加载对应数据并返回给用户
write through
程序更新数据 a.没有缓存则直接更新 db b.有缓存则更新缓存,后续由 cache 自己 将数据刷入 db
补充一:考虑失败情况
其实对以上三中同步更新的场景都没有讨论缓存、db 分开的两个操作如果有失败(网络等不可控异常)如何处理。
这里简单探讨用户程序管理 cache 、 db 更新关系的 cache Aside模式。
write 流程 1:udpate db成功,update cache 失败 ----> cache 产生脏数据,通常不可接受
write 流程 2:delete cache 成功, update db 失败----> cache 产生一次 miss,通常可接受
write 流程 3:udpate db成功,delete cache 失败------>cache 产生脏数据,通常不可接受
在 58 的架构师 沈剑的文章中,重点考虑了失败情况下的代价问题,所以他重点推荐了 write 流程 2,对于流程 2 存在问题的解决思路是将同一 ID 的数据在服务层面路由到同一服务器上,在 db 连接池层面路由到同一连接上,保证 db 对同一 ID 数据读写操作顺序执行。
但此方案的实现代价,对负载均衡策略的的侵入都比较大。所以下面介绍一种异步更新的模式,并依赖 kafka 等中间件保证清除缓存的高可用性。
异步更新模式
接下来可以引入异步更新模式可以一定程度上缓解缓存更新失败的问题:
update----------->写 db
|
|
|
------------>监听 db 的 binlog并解析,发送kafka消息--------------> 消费并 delete cache
利用 kafka 消费的特性可以做到清理操作异常则重复消费,再次清理 cache
补充二:考虑多数据库的主从情况
一般对主库进行写入,而读操作都落在从库上,但是主从是有一定延时的,如果延时期间 cache 从从库中获取了数据,还是会导致脏数据问题。
所以对于cache aside 模式的三种写情况, cache miss 的情况下应读取主库
对于异步更新模式,监听的应该是从库的 binlog,多从库的情况下监听所有从库同步完毕,然后再发送 kafka 消息,这种模式下所有读流量可以放心的打到从库。
总结
在缓存、db 更新的操作不用事务保证原子性的情况下,总会有各式各样的问题,我们要做的就是充分考虑各种workround的成本取最优。
我们的服务中采用的方案为 write 流程 3 (先update db,再delete cache)+ 异步清缓存,缓存会清空两次。
好处:尽可能保证缓存第一时间更新 & 缓存和 db 的最终一致性
代价:增加了缓存 miss 的情况
reference:
缓存更新的套路

浙公网安备 33010602011771号