- 解决此问题主要思路
- db与cache操作过程可能产生的问题及解决方案
- 问题一:并发问题,数据不一致产生的主要原因(应用层问题)
- 根本问题
- 双写模式问题
- 失效模式问题
- 问题二:第一步操作成功,但第二步操作可能失败(应用层问题)
- 必须先更新db
- 如何更新/删除cache
- 如何重试
- 异步重试的方式是否会对双写模式和失效模式产生影响
- 问题三:db主从复制延迟导致的数据不一致(db层问题)
- 延迟双删解决方案
- 两种方案综合分析
- 双写模式
- 失效模式
- 结论
- 最佳实践
- 失效模式方案二:订阅数据库变更日志方案
- 是否有必要追求强一致性(各种场景下百分百保证一致性)
- 实践原则
- 具体实践方案
- 参考文献
解决此问题主要思路
同时操作数据库和缓存,使二者达成数据一致。主要的实现方式有以下两种
-
双写模式
改db(save,update,del),同时改一下cache
-
失效模式(推荐)
改db,同时删除对应cache,读取数据时再更新cache。更新cache的方式有两种,分别是手动更新与自动更新,以下主要考虑在应用层主动更新的情况,使用binlog自动更新cache的情况作为一种独立方案讨论。
但是以上两种操作方式在操作时会存在一些情况,下面具体展开分析。
db与cache操作过程可能产生的问题及解决方案 问题一:并发问题,数据不一致产生的主要原因(应用层问题) 根本问题db的修改与cache的修改无法保证原子性
双写模式问题如果出现多线程同时修改db的情况,网络延迟导致数据修改顺序与请求顺序错位,会导致cache修改错误。
如:AB两thread同时对数据进行更新,A先更新,B后更新
| 序号 | 正常逻辑 | 异常逻辑:网络原因A执行慢,B执行快 |
|---|---|---|
| 1 | A写入db,db为A | A写入db,db为A |
| 2 | A写入cache,cache为A | B写入db,db为B |
| 3 | B写入db,db为B | B写入cache,cache为B |
| 4 | B写入cache,cache为B | A写入cache,cache为A |
| 结果 | db中为B,cache中为B | db中为B,cache中为A |
出现此问题前提条件
- 双写并发
- 1最早发起写入的A thread 的更新时间变长
还是网络延时问题,导致db和cache操作非原子性
如:AB两thread同时对数据进行更新,C正常读取数据,A先更新(满足cache为空的前提),B后更新,C发起读取。
| 序号 | 正常逻辑 | 异常逻辑:网络原因A执行慢,B执行快 |
|---|---|---|
| 1 | A写入db,db为A | A写入db,db为A |
| 2 | A删除cache ,cache为null | A删除cache ,cache为null |
| 3 | B写入db,db为B | C读取cache,cache为null |
| 4 | B删除cache ,cache为null | C去db拉数据,拉到A |
| 5 | C读取cache,cache为null | B写入db,db为B |
| 5 | C去db拉数据,拉到B | B删除cache ,cache为null |
| 6 | C更新cache,cache为B | C更新cache,cache为A |
| 结果 | C读取结果为B,db为B,cache为B | C读取结果为A,db中为B,cache中为A |
出现此问题前提条件
- 只有读写并发(cache初始为null,B写thread、C读thread并发)才会出现
- C thread读取数据时,cache刚好为null
- C读数据的过程中,刚好有B thread在修改db
- B更新db时间 + 删除cache的时间 < C读取db时间 + cache赋值的时间
先更新/ 删除 cache,后更新db方案不可用,因为第二步db更新可能不成功。此时如果cache更新成功,但db更新失败,那么此时cache中是新值或null,但db中是旧值。只要cache过期失效,就会从db中读取到旧值,数据更新失败
结论:无论使用双写模式还是失效模式,只能先更新db,后更新/删除cache
如何更新/删除cache-
正常写cache。
-
如果更新cache失败,则需进行重试。
- 如果当前线程直接重试,问题在于
- 立即重试很大概率「还会失败」
- 「重试次数」设置多少才合理?
- 重试会一直「占用」这个线程资源,无法服务其它客户端请求
- 如果此时项目「宕机」了,待更新数据直接丢失
- 更好的方案建议采用异步重试,把重试请求写到「MQ」中,然后由专门的消费者来重试
- 重试时间和次数由当前线程控制
- 重试交给mq,可以释放当前线程资源
- MQ本身可靠性,可以保证数据不丢失
参考问题一中的问题场景
- 双写模式:当A、B两thread顺序修改db和缓存,均采用先写db后写cache的方式更新,此时若A写cache出现失败重试,在完成重试前,B thread很可能已经完成了db和cache两步的更新,所以采用异步的方式对双写模式影响较大
- 失效模式:失效模式问题案例中A thread存在的意义在于创造特定初始值(cache初始为null,db值为A),所以只需分析B thread和C thread关系即可,C thread由于是read thread 所以不存在更新db成功,更新cache失败的场景,所以不会造成数据不一致,唯一的修改db和cache的只有B thread。而失效模式出现数据不一致的场景,只有当B更新db时间 + 删除cache的时间 < C读取db时间 + cache赋值的时间才会出现,使用MQ本身就拉长了B更新db时间 + 删除cache的时间进一步避免了数据不一致场景的出现。
针对问题一和问题二分析,综上使用失效模式更有利于天然保障数据一致性
问题三:db主从复制延迟导致的数据不一致(db层问题)问题三考虑是纯db层的问题,此问题独立于问题一和问题二出现,当应用保证数据一致性后,在db层仍可能出现问题,此问题无论是使用双写模式,还是失效模式都有可能出现
如:A thread对数据进行更新,B thread读取数据,db值原始为C
| 序号 | 正常逻辑 | 异常逻辑:网络原因A执行慢,B执行快 |
|---|---|---|
| 1 | A写入db「主库」, 主库db为A | A写入db「主库」, 主库db为A |
| 2 | A删除cache ,cache为null | A删除cache ,cache为null |
| 3 | B读取cache,cache为null | B读取cache,cache为null |
| 4 | B去db「从库」拉数据,拉到A | B去db「从库」拉数据,拉到C |
| 5 | B更新cache,cache为A | B更新cache,cache为C |
| 结果 | db中为A,cache中为A | db中为B,cache中为C |
由于前序问题已证明失效模式对于保障数据一致性更优,所以此处只分析失效模式下的延迟双删策略。
延迟双删策略即 A thread 可以生成一条「延时消息」,写到MQ中,消费者延时「删除」缓存。延迟时间理论上需保证
- 延迟时间要大于「主从复制」的延迟时间。此时再删除cahce可以保证下次read 从db拉值的时候一定是新值
但是,这个时间在分布式和高并发场景下,其实是很难评估的,很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。
两种方案综合分析 双写模式- 双写模式并发问题出现的前提,一般是双写并发。只要满足最早发起写入的A thread 的更新时间变长即会出现这种问题
- 双写模式会频繁更新cache,导致cache中存放了很多无用数据,cache利用率低
- 如果要保证不会出现类似问题,需要使用分布式锁(读写锁即可),使得最早一次A thread更新操作未全部完成前,另一B thread阻塞。但所有数据库更新全部变成单线程,此时会降低并发能力
- 失效模式并发问题出现的前提,一般是读写并发。且要满足B更新db时间 + 删除cache的时间 < C读取db时间 + cache赋值的时间,但写db mysql会自动加锁,同时数据库多副本的情况下,更新数据需要主从同步,所以写时延>>>读时延。所以此种情况发生的概率极低
- 如果要保证不会出现类似问题,需要使用分布式锁(读写锁即可),使得最早一次A thread更新操作未全部完成前,另一B thread阻塞。但所有数据库更新全部变成单线程,此时会降低并发能力
失效模式在非极端情况下,可以认为是天然满足db与cache缓存一致的。双写模式有较大概率出现数据不一致问题
最佳实践 失效模式方案二:订阅数据库变更日志方案业务应用在修改数据时,「只需」修改数据库,无需操作缓存。当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,redis拿到具体操作的数据,然后再根据这条数据,去删除对应的cache。例如阿里的canal中间件
是否有必要追求强一致性(各种场景下百分百保证一致性)强一致性解决方案,双写模式和失效模式都可以通过加读写锁的方式,保证数据一致性。但所有数据库更新全部变成单线程,极大降低并发能力,甚至很可能会超过引入缓存带来的性能提升
实践原则- 经常性修改的数据,就不要放缓存,直接读数据库。避免频繁更新缓存
- 触发读更新时,设置缓存过期时间。保证缓存利用率
- 采用失效模式。天然满足数据一致性
- 根据需要选择手动删除cache还是自动删除cache,如果要手动删除cache,引入MQ参与重试机制重试可靠消息投递
根据业务场景选择做一致性和性能的trade off
-
数据是否会有大并发
如:用户操作维度的数据,并发可能性小。一般给缓存加过期时间,一段时间后触发读更新即可
-
如果数据会出现并发,考虑对用户重要性,客户读到过期数据是否会产生重要影响
如:菜单栏、商品介绍等数据,是旧版还是最新版对使用者影响小,可使用canal订阅binlog方式
-
绝大部分场景解决方案:给缓存加过期时间即可,最多再考虑一下失效方案,分布式锁的方案大并发场景下不要用
https://zhuanlan.zhihu.com/p/408515044



