Redis和Mysql一致性保证
1.为了解决数据库压力过大的情况,首先引入redis,让大部分读请求在redis读,写在mysql里
2.那刚开始采用全量刷新,这样MYSQL数据全部在redis里,此时读全都能在redis中命中,写在数据库中
-
缓存利用率低:不经常访问的数据,还一直留在缓存中
-
数据不一致:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的执行频率)
为了解决缓存数据都是热点数据
给缓存数据设置一个过期时间,写还是写数据库,读的话还是读redis,读不到去mysql读,并同步到redis
为了实时性
放弃刷盘同步策略,采取写数据库的时候同时写缓存,但是这样会有问题。
数据库和redis更新的话谁先谁后呢?
1.不考虑并发的情况下 假如第二步失效
先写数据库再写redis的话
数据库写成功了,redis写失败
那么去读的时候,读到的旧数据,直到失效才能读到更新后的数据
先写缓存再写数据库
缓存写成功,数据库写失败
当我再去读的时候发现读到的是新值,但是一旦失效,读到的是旧值
2.考虑并发情况,两步都成功
两个线程在同时写的时候,无论是数据库缓存更新前后顺序,都会出现数据库缓存不一致性出现
但是上锁就不能保证效率了
所以考虑另一种方法——不更新缓存了而是删除缓存
删除缓存策略
先删除缓存还是先更新数据库呢?
1.假如先删除缓存再更新数据库,在读写并发的时候
-
线程 A 要更新 X = 2(原值 X = 1)
-
线程 A 先删除缓存
-
线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
-
线程 A 将新值写入数据库(X = 2)
-
线程 B 将旧值写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。
2.先更新数据库,后删除缓存
依旧是 2 个线程并发「读写」数据:
-
缓存中 X 不存在(数据库 X = 1)
-
线程 A 读取数据库,得到旧值(X = 1)
-
线程 B 更新数据库(X = 2)
-
线程 B 删除缓存
-
线程 A 将旧值写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。
但是这个概率很低
其实概率「很低」,这是因为它必须满足 3 个条件:
-
缓存刚好已失效
-
读请求 + 写请求并发
-
更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)
总结1:
所以我们现在选择写数据库然后删除缓存策略,既不会出现同时并发写的数据库缓存不一致,
也很难出现读写并发的数据库缓存不一致。
保证第二步执行成功,防止出现读的错,最后读的对或者读的对,最后读的错。(不一致)
如何保证第二步成功?
重试啊?可是重试的话,不能立即重试吧,只能说等一会重试,那么就会占据资源。
所以采取MQ机制去异步删除缓存。但是也太麻烦了,每次修改数据库我还得发送消息到消息队列里。
所以采取canal来自动发送消息到MQ里,让MQ去发送消息去删除redis
此时也不能解决一致性问题
为什么呢?
一种就是前面说的先删除缓存然后再去更新数据库出现的数据不一致。
这种不是被淘汰了嘛,为什么还要说呢,是为了说明旧值写回问题(读写并发,先写中间读穿插)
第二种,就是mysql主从集群架构下,主从复制问题,写的时候是在mysql的主库去写,读的时候是在从库去读,主从之间的数据同步是需要时间的,然后的话如果你刚才不是采取先写数据库再去删除缓存
这时候一个线程去写主数据库了,然后删除了缓存,第二个线程瞬间过来了,然后去读数据的时候,会去从库读数据的时候发现什么问题呢??主库更新的数据没更新过来呢还,于是就直接拿到的是原来的旧值,并且然后更新到缓存里,这时候缓存是旧值,对吧,过一会,主库更新过来数据了,此时数据库里面是新值,出现了缓存,从数据库不一致情况。
为了解决这些问题,第一个解决方案是缓存双删,删一次过一会再删一次就是为了解决前面的问题,第二个解决方案是延迟删除,删除的时间要大于主从复制的时间,大概是1~5s。
他们防止的都是旧值写回问题。
最后我们采取了延迟删除+canal+mq的形式进行数据库同步
但是还有什么问题没解决呢?
1.极小概率的读写并发下的不一致问题
2.延迟缓存双删策略 延迟时间或者二次删除不一定准确
还是会出现不一致情况。但是那又有什么关系呢?引入缓存就是为了效率,强一致性的保证不了效率(例如分布式事务Seata的XA模式,最好还是为了中和而出现了AT模式,TCC,Saga模式)