数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

数据一致性问题

写作背景:看了些视频,网上也看了很多篇文章,感觉写得都很片面,不是很全,所以我整体总结了一些,我不喜欢重复造*,对于网上很多篇复制机类型的文章(很多文章所有字体相同)且很多不正确严重误导他人,我很是抨击这样的行为,这样是严重浪费了别人宝贵的生命时间去学习!当然我也不能保证我这篇文章就写得全部正确,或者很全面,但是我尽了我最大的努力去思考,并在我的知识范围内作出最优解答,我不会随手不思考就复制别人文章然后误导别人这种行为,对于这种行为我也是严重抨击。对于我的这篇文章,如果有写得不对的地方也麻烦各位网友及时指正,也很希望各位给我纠纠错,学文章的目的也只在于互相学习,互相进步!
此篇文章主要是站在redis+mysql的角度考虑数据一致性,当然其它牵扯缓存与db的数据一致性问题,也可以得到借鉴,因为原理和问题都相同


对于redis与数据库的数据如何达到数据一致性,我们平常都会采用如下的处理方案,但是这几种方案在极端情况下其实都是有问题的,下面我们就看看我们平常书写的几种方案中所存在的问题:

1、先更新数据库,再更新缓存:
问题1:如果先更新数据库,然后再去更新缓存失败时,则将导致数据库是新数据,而缓存的数据还是老数据,出现了数据不一致性的问题
  解决:事务,更新缓存失败则数据库也失败,达到数据一致性
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

问题2:线程A先更新数据库,然后再更新缓存,这个时候线程B也更新数据库,由于网络延迟,线程B在线程A之前更新缓存,线程A最后更新缓存,则缓存中的结果和数据库的结果不一致
  解决:互斥锁/队列+事务
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

网络上针对先「更新数据库,再更新缓存」问题的劣质解决方案:先删除缓存,再修改数据库。此方案依旧带来了许多问题!往下看!

2、先更新数据库再删除缓存:
网络上针对:「这里为什么不是更新缓存,而是失效」问题的解答:主要是怕两个并发的写操作导致脏数据:例如线程A更新db,线程B进来更新db并且更新redis,这个时候线程A再更新redis,写操作导致了老数据。但是删除缓存也会导致老数据问题的存在!这个方案并没有完美的解决了数据不一致性问题,而且比更新缓存多引出了一个问题:那就是客户端的读取操作必须维护当缓存为null时而走数据库读取的操作,如果走数据库又存在缓存穿透,和缓存击穿的问题需要解决。可能有人会有疑问,难道我更新缓存就可以避免缓存穿透和击穿的问题?对,在更新数缓存的时候,只要不是redis宕机,就可以保证缓存中必然有数据,就可以不因为缓存为null而走数据库的操作;但是失效缓存却是必然要走数据库操作!!!如果redis宕机了怎么办?针对这种情况可以在redis重启时用预加载数据统一处理缓存为null的情况,避免了分散在每个缓存点都需要维护缓存为null所带来的问题!

问题1:删除缓存失败,则缓存是老数据
  解决:事务特性
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

问题2:缓存失效,线程A读,从数据库中读取数据,接着线程B写操作且使缓存失效,再接着线程A将读到的数据写入缓存,则缓存是老数据
  解决:互斥锁/队列+事务特性
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

3、先删除缓存,再修改数据库存在:
此方案利用的是:删除缓存成功,即使修改数据库失败,下次客户端读取的时候,发现缓存为空就从数据库中读取再刷新到缓存中的特性保证了一致性。看似完美,其实也存在问题。唯一的好处就是:先删除缓存的话可以不需要事务辅助来达到数据一致,因为先删除缓存,接下来更新数据库失败也不要紧,因为会重新获取并刷新缓存。
  问题:删完缓存,线程把老数据再读取到缓存,然后再更新数据库,缓存中依然是老数据。

数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

解决方案1:延时双删:先删除缓存,再修改数据库,修改完数据库之后,再延迟删除一次缓存
疑问:为什么是延时删除?因为要保证在读取线程读取db写入缓存之后再删除
问题: 1、第二次删除,如果失败,那么缓存中的数据依旧是老数据
   2、无法确定延迟多久
   3、存在一段时间的数据不一致(这个方案是达到最终一致性):在第5步和第6步之间的时间里,用户所读取到的缓存数据依旧是不一致的
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

解决方案2:串行化:先删除,再更新,再读,保证缓存是从数据库最新获取的
问题:1、串行化会降低系统吞吐量
  2、处理时间拉长:用户读取数据必须等待更新完毕之后才能读取得到
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解
4、先更新缓存,再修改数据库存在的问题:
这种方案比不过「先删除缓存,再更新数据」因为删除了缓存,即使更新db失败,也会可以从数据库重新获取,保证了后续的一致性;但是更新缓存,如果更新db失败,那么缓存将直接不一致,而且事务保证缓存一致性也无法介入,所以这是最差的方案
问题1:更新完毕缓存(即使存在事务),再修改数据库失败,则缓存与数据不一致。
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

问题2:线程A更新缓存,线程B更新缓存,线程B更新数据库,线程A更新数据库。导致缓存不一致。缓存中的数据是新的,但数据库中的数据是老的
  这个问题可以用互斥锁/队列解决,但是无法保证更新db失败。更新db失缓存无法撤销更新,所以无解!或许可以先记录之前的缓存数据,然后更新数据库失败再回退缓存数据,但是这样的复杂性操作还不如直接【先更新数据库】然后利用事务来达到效果方便,没必要做这样的无用功,所以算是无解!
数据一致性问题、redis如何与数据库保持一致性问题,完整版详解

针对以上四种方案的优劣评估:
  四种方案都不是原子性操作,只要不是原子性操作都存在数据不一致问题!
  删除缓存比更新缓存:多引入了维护redis穿透、击穿等问题
  删除缓存好处:1、读多写少的情况下,根据懒加载的思路,可以节约每一次更新数据的时候都要去更新缓存,而且没有人访问所消耗的更新缓存性能
  网络上针对为什么是删除缓存,而不是更新缓存?利用缓存失效再次重读保证数据一致性,如果是更新的话如果更新数据库失败了,那将导致缓存不一致。不过删除缓存依然存在缓存不一致情况

  「先更新数据库,再更新缓存」:存在两点问题。但是利用事务+互斥锁/队列可解决。
  「先更新数据库,再删除缓存」:存在两点问题。但是利用事务+互斥锁/队列可解决。多引入了维护redis穿透、击穿等问题
  「先删除缓存,再更新数据库」:存在一点问题。利用串行化可解决。多引入了维护redis穿透、击穿等问题。可以不需要事务。
  「先更新缓存,再更新数据库」:存在两点问题。无解。有复杂的方案,但是还不如先更新数据库利用事务特性简单,没必要做这样的无用功,所以算是无解!

总结:通过上面的评估,各种场景优先方案如下
  如果数据库不采用事务,那么第一的方案是「先删除缓存,再更新数据库」;
  如果数据库采用了事务,但是是读的场景为主,那么方案优先次序为:「先更新数据库,再更新缓存」、「先更新数据库,再删除缓存」、「先删除缓存,再更新数据库」
  如果数据库采用了事务,但是是写的场景为主,那么方案优先次序为:「先更新数据库,再删除缓存」、「先更新数据库,再更新缓存」、「先删除缓存,再更新数据库」
    (之所以把「先更新数据库,再更新缓存」放在「先删除缓存,再更新数据库」前面是因为后者既对代码侵入性、和提高复杂,而且效率降低(在维护redis穿透和击穿时需要互斥),而前者只是效率降低)
  无论哪种情况都不应该采取的方案:「先更新缓存,再更新数据库」


其它方案:
1、更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的
问题:可能会存在丢失数据,例如redis宕机

绝对保证强一致方案:
  1、2PC
  2、主从同步复制,入队列执行,再用事务,先更新数据库,再更新缓存。如果在更新数据库失败的话,缓存还是和数据库一致,如果在更新缓存失败的话,也是和数据库一致


整体总结:
  只要不是原子性的操作都存在问题
  我们能放入缓存的数据本身不应该是实时性、一致性要求超高的。所以缓存数据的时候可以加上过期时间,保证每天拿到当前最新数据即可
  我们不应该过度设计,增加系统的复杂性
  遇到实时性、一致性较高的数据、就应该查数据库,即使慢点