今天回顾数据库和缓存的一致性的场景有所感悟,因此有记:
不管是先删除缓存再写入数据库,还是先写入数据库再删除缓存,都有可能出现不一致的情况:
(1)先删除缓存再写入数据库
数据库X = 1
线程A修改X=2:1、删除了缓存, 3、写入X=2
线程B查询: 2、 发现缓存无数据,于是从数据库查询,X=1,写入缓存
最终结果:缓存X=1,数据库X=2
(2)先写入数据库再删除缓存
数据库X = 1
线程A查询: 1、 发现缓存无数据,于是从数据库查询,X=1, 3、写入缓存
线程B修改X=2: 2、更新数据库X=2,删除缓存
最终结果:缓存X=1,数据库X=2
当然(2)先写入数据库再删除缓存这个出现不一致的概率很低,因为要”恰好“的有读写并发,而且读的线程比写的线程还慢,写线程本身就要加锁。对比起来(1)先删除缓存再写入数据,只要有读写并发就很容易出现数据不一致的问题,已经算是一个很好的解决方案。
但是还要考虑一个问题,使用(2)的话,就是期间可能发生一些事故导致写入数据库之后服务器无法正常执行下去,重启后会存在数据库是2但是缓存还是1的问题,但是如果是用(1)的话,先删除缓存之后异常,那么恢复之后的话缓存还是会去读数据库的1,好歹还是一致的,但是这导致了结果是旧值。这就是剩余的第二步执行失败的问题了。
所以在得出使用(1)是更好的方式的前提下,如何在(1)的第二步:删除缓存 下功夫?
1、重试,使用第三方重试,这已经算是比较受大家接受的方案了,比如使用MQ删除。使用MQ不仅仅异步,还节省了工作线程的时间,毕竟重试多少次?多久?总不能卡在这吧
2、使用如canal等工具直接从数据库,如mysql中直接拉取对应的binlog,根据这个binlog再对缓存进行删除
所以现在总结下来比较能让人接受的总体方案大概为:
先写入数据库,再删除缓存
(一)(第二步的删除缓存可以做优化,1、MQ执行删除 2、canal读取数据库binlog再交给MQ执行删除)
其次还有主从延迟问题
数据库X=1
线程A执行X=2:1、X=2写入数据库,删除缓存 3、同步至从库中
线程B查询: 2、查询缓存为空,从库中查询到X=1,写入缓存
最终结果:缓存X=1,数据库X=2
这个问题其实解决方式和(一)一样,都是执行缓存删除就能解决这个问题,往后延迟删除就肯定解决这个问题
但是问题就是,不知道延迟多少才好,很难评估,这个需要参考生产环境才能做到的效果
先暂停一下,回到我们的一致性问题,那么我们现在做到这个步骤了,发现了没,其实我们的效果都是达到的是最终一致性
因为我们的服务器一直在运行,线程一直在工作,有没有必要做到强一致性呢?只要查询的时候就把所有修改操作都阻塞掉?不合理吧。
但是还是有强一致性的方案如2pc,3pc等一致性协议。