缓存与数据库的一致性

applex 2019-12-08

https://blog.csdn.net/huazhongkejidaxuezpp/article/details/88945627

背景
           cache如memcache,redia等缓存来缓存数据库读取出来的数据,以提高读性能。但凡是使用缓存的项目,几乎都会遇到一个普遍的问题: 在不断增删改数据的过程中,如何保持缓存与数据库中数据的一致性。在支付、下单类业务中,此类问题尤为普遍。下面就自己对此的一些理解。浅谈一下自己的看法。

缓存的衡量指标
命中率、响应时间;缓存一致性。

命中率:请求的命中率与miss率。可以通过一定的工具监控到。

响应时间:包括命中时的响应时间,非命中时的响应时间。

缓存一致性:在不断进行增删改操作后,缓存中的数据是否与数据库数据保持一致,避免二者不一致,获取数据不正确的情况发生。

使用缓存可能带来的问题
         分布式环境下非常容易出现缓存和数据库间的数据一致性问题。在使用缓存前,如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。

         经典的几个问题如下:

1)缓存和数据库间数据一致性问题

问题描述:缓存和数据库间的数据不一致。缓存与数据库不一致的情况,大致分为三类:

数据库有数据,缓存没有数据;

数据库有数据,缓存也有数据,数据不相等;

数据库没有数据,缓存有数据。

合适的策略:包括 合适的缓存更新策略,更新数据库后要及时更新缓存、缓存失败时增加重试机制,例如MQ模式的消息队列。

2)缓存击穿问题

问题描述:缓存击穿表示恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。

场景举例:抢购活动、秒杀活动的接口API被大量的恶意用户刷,导致短时间内数据库宕机了

解决方案:

使用互斥锁排队
业界比较普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)就够了。

从一定程度上减轻数据库压力,但是锁机制使得逻辑的复杂度增加,吞吐量也降低了,有点治标不治本

布隆过滤器(推荐)
bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小

3)缓存雪崩问题

问题描述:缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。

解决方案:

加锁排队
建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存;
4)缓存并发问题

问题描述:多个redis的client同时set key引起的并发问题

解决方案:

把redis.set操作放在队列中使其串行化,必须的一个一个执行
加锁
缓存使用/更新套路
1. 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就更新数据库,并将数据会写到缓存,并返回。

2. 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。

进一步思考:
先删除缓存,然后再更新数据库:如果A,B两个线程同时要更新数据,并且A,B已经都做完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,由于缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致

为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。

效果:理论上也是有不一致的风险的,但概率很小。不一致原因:产生的原因是更新数据库成功,但是删除缓存失败。可以进一步考虑的方案:

1. 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。

2. 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。

3. 给所有的缓存一个失效期。失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定

进一步思考,当更新数据库时候,缓存应该如何更新  
更新缓存VS淘汰缓存

     答:更新缓存很直接,但是涉及到本次更新的数据结果需要一堆数据运算(例如更新用户余额,可能需要先看看有没有优惠券等),复杂度就增加了。而淘汰缓存仅仅会增加一次cache miss,代价可以忽略,所以建议淘汰缓存。 当然了可以采用更新缓存而不是淘汰缓存,前提是更新的代价比较低

测试思路
          测试缓存的时候,除了业务在正常情况下需要正常交互外,尤其是热点key过期的时候,需要测试击穿,以及雪崩,穿透等情形对下游DB的并发请求带来的影响。

          测试场景:

1、命中情况下:响应情况

2、并发下、非命中下响应情况

思考点

测试点

(雪崩)缓存会不会集中失效?如会,怎么避免?

1、 (如果会失效)模拟集中失效场景

2、查看服务响应情况

3、如果有措施,测试措施的效果。

措施例如:

在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。
缓存key是什么?为什么?

1、key的内容

2、缓存占用的大小

会不会出现缓存穿透?如何避免?

缓存穿透是指查询一个一定不存在的数据

1、模拟穿透情况

2、查看服务响应情况

3、测试应对方案:

1)使用互斥锁

2)从数据库找不到的时候,我们也将这个空对象设置到缓存里边去

3)布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层

如果缓存挂了,业务是否会有影响?怎么控制这里的影响面?

1、模拟缓存挂了的情况

2、查看服务响应情况

3、测试应对缓存挂掉的方案:

事发前:实现redis的高可用,尽量避免缓存全部挂掉

事发中:万一redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),确保业务正常

事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。

什么时候更新缓存?如何确保更新成功?

1、模拟业务(并发)各种增删改查的情况

2、检查缓存更新情况

3、测试不同更新策略

ps-两种策略各自有优缺点:

先删除缓存,再更新数据库:在高并发下表现不如意,在原子性被破坏时表现优异;
先更新数据库,再删除缓存(Cache Aside Pattern设计模式):在高并发下表现优异,在原子性被破坏时表现不如意
失效时间是多少?为什么?

1、失效时间设置多少

2、失效后影响

缓存方案命中率/miss率多少

1、模拟缓存使用场景

2、评估命中率/miss率

3、使用工具监控命中率/miss率

相关推荐