缓存和DB中数据一致性
淘汰还是更新
一般来说,是淘汰。
- 一般来说,修改的成本会高于淘汰的成本
- 修改的话,假如存的是json字符串,需要先将数据反序列化,然后修改数据,然后序列化,再存入redis。修改的代价高,但是少一次cache miss
- 淘汰的话,就将数据置为无效,但是多一次cache miss
- 修改缓存,在并发写的时候,可能出现数据不一致
- 比如说请求1 先写数据库,然后请求2写数据库
- 接着请求2更新缓存,请求1更新缓存
- 这就造成数据库是请求2的修改结果,缓存是请求1的修改结果
- 一般的模式为:
- 更新了数据,就直接淘汰掉缓存
顺序问题
是先操作缓存,还是先操作DB?
先操作缓存,再操作DB
先淘汰缓存成功,后更新DB失败(比如说服务挂了),不会造成不一致。
但是缓存淘汰了以后,主库还没有同步到从库,又有一个读请求,把旧的数据读到缓存,也会造成不一致。
这种情况下不一致概率是比较高的,因为一般情况下读请求远远高于写请求,当淘汰了缓存之后,在更新DB之前很有可能有读请求把从库的旧数据读到缓存中,从而造成不一致。
不过对于这种情况,有以下两种办法:
- 给缓存设置过期时间,能达到最终一致性
- 监听主库bin log,当主从同步完成,再淘汰一次缓存
但是这样子代价就比较高了,架构变得复杂。
先操作DB,后操作缓存
先更新DB,后更新缓存。
假如更新完DB后,服务挂了,没有更新缓存,缓存过期后,经历一次缓存miss,那么数据将达到最终一致。
DB主从延迟导致的缓存不一致
背景
缓存与数据库不一致
如上图,发生的场景也是,写后立刻读:
(1+2)先一个写请求,淘汰缓存,写数据库
(3+4+5)接着立刻一个读请求,读缓存,cache miss,读从库,写缓存放入数据,以便后续的读能够cache hit(主从同步没有完成,缓存中放入了旧数据)
(6)最后,主从同步完成
导致的结果是:旧数据放入缓存,即使主从同步完成,后续仍然会从缓存一直读取到旧数据。
可以看到,加入缓存后,导致的不一致影响时间会很长,并且最终也不会达到一致。
方案
如上图所述,在并发读写导致缓存中读入了脏数据之后:
(6)主从同步
(7)通过工具订阅从库的binlog,这里能够最准确的知道,从库数据同步完成的时间
画外音:本图画的订阅工具是DTS,也可以是cannal订阅和分析binlog
(8)从库执行完写操作,向缓存再次发起删除,淘汰这段时间内可能写入缓存的旧数据
(这样子还是在短时间内可能存在缓存和DB不一致,但是能达到最终一致性)
Cache Aside Pattern
Cache Aside Pattern是缓存经典实践方式,分为读实践、写实践。
对于读请求
- 先读缓存
- 缓存命中,直接返回数据
- 缓存未命中,则查询DB
- 将数据set到内存
对于写请求
- 先更新数据库
- 后淘汰缓存
总结
- 先更新DB,再更新缓存
- 尽量淘汰缓存
- 可以采用纯内存数据库,然后异步更新到DB(要求高可用)