如何保证redis与db的双写一致性

引言

    如何保证redis与db的双写一致性?这是一个十分热门的面试话题。 如何理解“一致性”这个概念?“事务”中“一致性”的定义是: 事务执行前后,数据从一个合法性状态变换到另外一个合法性状态。  比喻说,更新前:redis中记录的是100,db中记录的也是100。更新后: redis中记录的是 80,db中记录的也是 80。 

一、问题场景假设

在同时更新redis和db时,可能出现更新某一个失败或者更新不及时,导致双写一致性问题。

比喻说,更新前:redis中记录的是100,db中记录的也是100

二、基本原则

1、数据必须优先落库db。redis只是db的数据缓存。

2、做更新操作时,数据必须以db的为主。不能以redis查询的数据,去做计算再更新db。

3.  更新db时,为了保障隔离性。需要加锁(基于版本号的乐观锁或者for update悲观锁)

4.  查询时先查询redis,如果redis未命中,则继续查db。再把查询结果写回redis

三、可能的解决办法

1.先更新缓存,再更新数据库

问题一:更新redis成功+更新db失败。

案例更新结果: redis中记录的是 80,db中记录的也是 100

不可取。除了没有保障一致性,而且违背了原则1

2.先更新数据库,再新缓存

问题一:更新db成功+更新redis失败

案例更新结果: redis中记录的是 100,db中记录的也是 80。

问题二:更新db成功+更新redis不及时

正常情况:

  1.  线程A更新db为80
  2.  线程A更新redis为80
  3.  线程B更新db为90
  4.  线程B更新redis为90

异常情况1

  1.   线程A更新db为80
  2.   线程B更新db为90
  3.   线程B更新redis为90
  4.   线程A更新redis为80

案例更新结果: redis中记录的是 90,db中记录的也是 80。

3.先删除缓存,再更新数据库

问题一:删除redis成功+更新db延迟

异常情况2

  1. 线程A删除redis缓存
  2. 线程A更新DB为80。执行完需要5秒
  3. 线程B在A更新DB第1秒开始执行查询。查询redis没命中,再查询db为100,再更新redis为100

案例更新结果: redis中记录的是 100,db中记录的也是 80。

4.延迟双删(先删除redis,再更新db,再删除redis)

延迟双删,可以解决异常情况2

异常情况3:

  1. 线程A删除redis缓存成功
  2. 线程A更新db为80。执行完需要5秒
  3. 线程B在A更新DB第1秒开始执行查询。查询redis没命中,再查询db为100,再更新redis为100
  4. 线程A删除redis失败。redis宕机了。

为了解决第二次删除redis可能失败,可以使用消息队列或者canel订阅mysql的binlog日志等实现延迟删除 + 失败补偿

5.先更新数据库,再删除缓存

问题一:更新db成功+删除redis失败

异常情况

  1. 线程A更新DB为80
  2. 线程A删除redis失败
  3. 线程B执行查询。先查询redis,命中返回结果100

案例更新结果: redis中记录的是 100,db中记录的也是 80。

四、推荐解决方案

技术选型:

   mysql + rabbitmq + canel + redis

1.redis采用集群,实现高可用。

2.查询。先查询redis。未命中,则DCL(双检锁) 的方式,查询db。查询结果再写入redis

3.更新。

     3.1 更新db时,需要加锁。如加for update悲观锁,或者基于版本号的乐观锁。(防止多线程同时更新,相互影响)

     3.2 更新db时,set的字段取值,不能使用redis查询的结果,必须使用db的查询结果(避免读到redis的脏数据,而db如mysql的默认隔离级别是可重复读,不会读到脏数据

     3.3  更新顺序。必须优先保障数据落库db。可以分为:

        3.3.1   延迟双删(先删除redis, 再更新db,最后再删除redis)。

               在落地时,为了防止第二次删除redis失败,可以通过以下两种方案:

           3.3.1.1  基于消息队列

              在准备第二次删除redis缓存时,将其放入消息队列。消息队列消费失败,放入死信队列。预设死信队列失败重试次数。死信队列消费次数超过阈值,则日志报警。(消息队列也要搭建集群来保障可用性)

          3.3.1.2   canel订阅mysql的binlog日志

              通过阿里的canel中间件来订阅mysql的binlog日志,来实现异步删除。删除失败,则放入消息队列。

        3.3.2   先更新db,再删除redis。

              先删除db,然后删除redis。如果删除redis失败,则放入消息队列。消息队列消费失败,则放入私信队列。

    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值