探讨缓存一致性问题
本文只探讨只读缓存,即只对缓存进行读取、写入、删除,不进行更新操作
前言
数据库的读写性能上限是比较低的,工程中经常在数据库前面加一层缓存,可能是Redis或者本地缓存。既然有缓存,那么不可避免的会遇到缓存一致性问题。
缓存一致性的概念
缓存一致是指:缓存有值,且值等于数据库中的值,如果缓存中的值不等于数据库中的值,则认为是不一致。
缓存的操作场景
一般操作缓存有两种场景:新增和更新(修改、删除)。
新增
操作流程:
- 向数据库写入新的数据
- 读取时,缓存中不存在,则进行回源并更新缓存,这时数据库和缓存是一致的
更新
更新缓存的场景内其实还有两个细分场景,主要差别是更新的顺序不同。如下:
- 先更新数据库,后删缓存
- 先删缓存,后更新数据库
问题以及解决方案
新增场景下不会有不一致的问题,因为读取是从数据库读取并写入到缓存中的,所以始终是一致的。
更新场景下会遇到两个问题:
- 单线程下的操作失败问题
- 并发情况下的顺序问题
单线程情况
因为更新是有两个操作步骤,即更新数据库和删除缓存,如果后续步骤失败了,那就会才造成数据不一致,例如先更新数据库但是删除缓存失败了,那么后续的请求会直接读取到缓存中的旧值;反而如果是先删除缓存,但是后续更新数据库失败了,影响倒是不大,后续的请求发现缓存不存在,会回源正确的数据。
解决方案
消息队列+重试
将更新操作生成消息,暂存在消息队列中,当删除缓存成功后,将消息从消息队列中丢弃,当删除失败时,执行失败策略,从消息队列中取出消息进行重试,重试超过一定次数则上报。
订阅binlog变更日志
创建一个更新服务订阅数据的binlog变更日志,收到数据库变更后删除缓存
并发情况
并发情况下需要对两个更新顺序分别分析
先删除缓存 后更新数据库
线程A删除缓存后,线程B进行读取,发现数据不存在,则进行回源,此时缓存值是旧的,最后线程A才将更新的新值写入数据库。
解决方案
- 设置缓存过期时间+延时删除
设置缓存过期时间,这样数据过期后还可以再次拉取数据库进行更新,达到最终一致性。即使数据不一致,但造成的影响时间范围比较小,。或者使用延时队列在更新完数据库后再次进行删除缓存,这样即使在延时期间有其他线程读取了旧数据并更新缓存,后续也一定会再次进行更新。
先更新数据库,后删除缓存
线程A更新数据库后,还没删除缓存,这时线程B进行读取,读取到了缓存中的旧值。
或者如果数据库采用主从架构时,线程A更新完数据库并删除缓存后,线程B发现缓存无数据进而向从数据库拉取数据并写缓存,如果这时主数据库的数据还未同步到从数据库时,就会导致数据不一致。
解决方案
- 延时删除
更新完数据库后延时一段时间再进行删除,避免主从数据库同步延迟造成的数据不一致 - 订阅数据库binlog进行删除
可以订阅数据库的binlog,等从数据库全部同步完成后再删除缓存 - 加锁
将更新数据库+删除缓存合并成一个原子操作,进行加锁处理,线程A进行更新时,加锁,线程B判断有锁后,直接读取数据库数据进行返回,不再进行写缓存操作