缓存数据一致性-解决方案

加缓存无疑是减少数据库、服务器压力的一大神器,缓存里的数据一般都是稳定的,不容易更改的,但是有的时候,某些业务场景需要更改缓存中的内容,这必然就涉及到更改本地数据库中的数据,其中关键的一个点就是保证其数据库和缓存的数据一致保证为最新数据。目前数据同步方案有两种:双写模式失效模式,下面会分别介绍其具体内容和优劣。

一、双写模式:

双写模式就是在更改数据库后,在对缓存中的数据进行操作。可能有朋友会想到为什么不先改缓存再改数据库呢?这里分别介绍一下:

《【58沈剑架构系列】缓存架构设计细节二三事》58沈剑:

对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是:如果出现不一致,谁先做对业务的影响较小,就谁先执行。

假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss。

假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致。

书内说的内容并没有问题,但是没完全考虑好并发请求时的数据脏读问题,让我们再来看看孤独烟老师《分布式之数据库和缓存双写一致性方案解析》:

先删缓存,再更新数据库

该方案会导致请求数据不一致

同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

(1)请求A进行写操作,删除缓存

(2)请求B查询发现缓存不存在

(3)请求B去数据库查询得到旧值

(4)请求B将旧值写入缓存

(5)请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

所以先删缓存,再更新数据库并不是一劳永逸的解决方案,再看看先更新数据库,再删缓存这种方案怎么样?

先更新数据库,再删缓存这种情况不存在并发问题么?

不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

(1)缓存刚好失效

(2)请求A查询数据库,得一个旧值

(3)请求B将新值写入数据库

(4)请求B删除缓存

(5)请求A将查到的旧值写入缓存

ok,如果发生上述情况,确实是会发生脏数据。

然而,发生这种情况的概率又有多少呢?

发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。

先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会因为上面说的原因,变得比较低!所以如果不是对数据的实时性要求特别高的话,一般都是先更新数据库再操作缓存。如果非要保证一致性的话,我们一般只强调最终一致性,而不是理想中的完全一致,这个内容稍后会讲。

言归正传,开始说双写模式。

双写模式就是对数据库中的内容进行写入新的操作后,再set进缓存中的一份。但是这样做法有一个缺点如图所示:

《缓存数据一致性-解决方案》线程a,b对数据库中name的name字段修改为name=张三,a修改后去set进入缓存,因为网络波动加服务器机房较远,这次修改需要1.2s 这时再0.2s时候线程b再来进行了操作将name=张三改为name=李四,也去修改缓存,这次很快在0.8s就完成了操作,而0.2s后a对缓存的操作修改完毕,用户看到的还是name等于张三,不过这个数据已经是脏数据了,这就是双写模式的过程和其问题。

解决办法:对每一个数据加上过期时间,现在错了没事,因为数据稳定存在数据库中,等缓存中的数据过期了,用户请求发现缓存中没有了,则会去查数据库得到最新值并set进入缓存,缺点就是如果业务对数据一致性的要求很高则这种方案不行。当然也可以加锁,保证a修改完后,b才可以进行修改数据库内容然后修改缓存,不过对性能有影响,分布式下Redission的lock是处于阻塞状态,对于cpu有很大损耗。

二、失效模式

失效模式就是先对数据库中的内容进行修改,然后对缓存进行操作,这里的操作是删除缓存中的内容而不是修改。

《缓存数据一致性-解决方案》

 先说缺点:如图所示,线程a修改完数据库库后,删除缓存,这时候线程b来对数据库进行修改,结果因为这次IO很慢很慢事务还没有提交,线程c进来的时候发现缓存没有数据,就去读取a对数据库更改的数据然后更新到缓存中,如果这次更新操作很快,线程b还没更新完数据库呢这时,并没有什么影响,无非就是更新了一个错误数据,后面线程b对数据库操作完就会删除掉缓存,等到下一个请求进来就可以得到正确数据了。

对于失效模式还有一种叫延时双删,就是先删缓存、再更新数据库延时500毫秒、更新完再删一次缓存防止在这个写入期间缓存被写入了新的数据。

缺点:比如线程A删除缓存然后线程B查询数据库获取旧值然后线程B更新了缓存,这时线程A更新数据库并线程A延时删缓存,前三步执行后,数据库和缓存是一致的,相当于没删除。后两部先更新数据库,再删缓存。所以延时双删演变成了先更新数据库,再删除缓存,问题还是没解决。为什么?假设,此时,在第4步执行之前,又来了个查询C,C查询到旧值。第6步:C将旧值插入缓存。此时出现缓存和数据库不一致

解决方法:1.加锁(系统笨重,不推荐)2.先思考这数据是不是要经常修改,如果经常修改直接读数据库就好了这样比用缓存还要加锁都要好。

数据库缓存一致性:

1.如果是用户维度数据(订单数据、用户数据),这种并发几率很小,(因为我们只是注册使用人多,但是不可能某个人在一秒内修改一万次自己的个性签名对吧),所以我们不考虑,缓存数据加上过期时间,每隔一段时间出发读的主动更新即可。

2.如果是菜单,商品介绍等基础数据,这样的对于一致性要求不高的数据(例如京东发现Iphone13修改了描述,京东也修改,用户又不知道我们什么时候修改)可以使用canal订阅binlog的方式

3.缓存数据+过期时间一般足以解决大部分业务对于缓存的要求。

4.通过加锁保证并发读写,写写eider时候按顺序排队。读读无所谓,所以这样的更适合使用读写锁。(业务不关心脏数据,允许临时更改脏数据可忽略)。

Canal是什么:

是阿里开源的一个中间件,模拟mysql的从服务器,只要mysql进行了数据变化,就会在binlog中记录,这是canal就把这个更新拿来,然后就自动去把缓存中的数据更新。这样的好处是我们不用关心缓存,只用关系数据库方面即可。

缺点:加了中间件,又要加一些东西配置。

一般canal是解决大数据的数据异构问题,比如小A喜欢购买衣服,小B喜欢购买电子产品,她俩都打开购物App,首页推荐的内容一定是不同的。怎么实现呢:假如有一个表记录了每个人浏览了什么类型的商品,cannal就去订阅这个表和数据库的商品表,实时知道那些商品进行了变化然后进行分析计算,生成另外一个异构系统的用户推荐表,然后web就从这个表中拿来数据进行展示推荐,减少了大量的计算。

总结:

我们能放入缓存的数据本就不应是实时性的、一致性要求很高的,所以缓存数据的时候要加上过期时间保证每天拿到最新的即可。

我们不应过度设计,增加系统复杂性。

遇到实时性、一致性要求高的数据,就应该直接查数据库,即时速度慢一些,(可以使用动静分离增大虚拟机内存等方式提高速度和吞吐量)。

    原文作者:liuxy1024
    原文地址: https://blog.csdn.net/m0_56466015/article/details/123404645
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞