Redis和数据库双写不一致的问题说的是,在进行数据更新时,Redis和数据库都要进行更新,怎么样才能保证在更新的同时用户读取的数据的一致性呢?我们根据具体的情况分析一下。
1、先更新数据库,再更新缓存(不可取)
若有两个请求P和Q先后过来更新数据:
(1)缓存刚好失效;
(2)请求P更新数据库;
(3)请求Q更新数据库;
(4)请求Q更新缓存;
(5)请求P更新缓存;
上述情况,P先与Q过来,因此更新完成后,数据库和缓存最终的值均因该为请求Q更新的值,而上述流程中,由于请求P在更新缓存是遇到了网络拥塞导致Q先更新了缓存,所以数据库中最终值的是Q更新的值,而缓存中最终值是P更新的结果,数据不一致。
2、先更新数据库,再删除缓存
小问题:为什么是删除缓存而不是更新缓存?
答:第一、删除缓存采取的是懒加载模式,删除后等待下一次查询请求过来才会更新缓存。若是每次更新数据库后,都更新缓存的话,会带来不必要的服务器压力,因为更新缓存可能涉及到一些计算,数据库连表查询等等。第二、第一个方案的并发问题。
该方式是facebook在论文《Scaling Memcache at Facebook》中提出,若有两个请求P和Q先后过来,P查询数据,Q更新数据:
(1)缓存刚好失效;
(2)请求P查询数据库,得一个旧值;
(3)请求Q将新值写入数据库;
(4)请求Q删除缓存;
(5)请求P将查到的旧值写入缓存;
上去确实发生了读写不一致的问题,但是仔细推敲一下:请求P先到达,发现缓存失效,然后去读取数据库。然后请求Q到达,下数据库,在删除缓存。在数据库中读操作会快于写操作,况且在进行读写分离的分布式环境下,读的速度会远快于写,而上述请求Q写数据库和删除缓存的操作,竟然发生在P写入缓存之前。从概率上讲,考虑到网络延时的问题,上述情况可能发生,但是概率极低。
在该方案下考虑上述并发问题,还不如考虑该方案下缓存删除失败的情况,来的更实在。若有两个请求Q和P先后过来,Q更新数据,P查询数据:
(1)缓存还未失效;
(2)请求Q将新值写入数据库;
(3)请求Q删除缓存失败;
(4)请求P查询缓存得到旧数据;
上述情况下,造成数据不一致状态,只能等到缓存过期或者新的写请求能删除成功,才能结束。
3、先删除缓存,再更新数据库
该方案很好的弥补了第二个方案中缓存删除失败带来的问题。在该方案中,若缓存删除失败,可以直接抛出异常,此后数据库也不会再更新,因此缓存和数据库一致。若数据库更新失败,数据库中还是旧数据,缓存还是会在下次读请求中被更新为旧数据,因此缓存和数据库一致。
但是,在高并发场景下,该方案仍然有一种情况需要考虑,若有两个请求Q和P先后过来,Q更新数据,P查询数据:
(1)Q删除缓存;
(2)P发现没有缓存,于是访问数据库;
(3)P从数据库返回旧数据,并更新缓存;
(4)Q更新数据库;
上述情况下,数据不一致。我们可以对上述方案进行改进。
改进方案三:先删除缓存,再更新数据库+jvm内存队列
改进中添加了一个内存队列(用被更新的数据进行唯一标记),当Q删除缓存后,在缓存被更新前,所有读和写请求都放入队列进行同步阻塞,等待缓存更新。指的注意的是,相同的写请求是没有意义的,可以忽略。还有一点值得注意,队列积压问题,要进行适当的控制。
4、先更新缓存,在将数据库更新请求放入kafka,进行一部处理
上述方案是一个补充方案,比较特殊,是我在项目项目中遇到的。该方案的主要思想是,将redis当做“数据库”来进行查询,其中的时间设置为永不过期,将真正的数据库当做备份。该方案要注意redis集群的高可用性。