Redis一致性,缓存击穿和雪崩等问题


使用Redis缓存所带来的好处:

1.降低后端的负载:

   对高消耗的SQL:join结果集/分组统计结果缓存

2.加速请求时间:

   在内存中做IO操作很快

3.大量写合并为批量写:

   频繁更新某一个值,可以在缓存层面统一处理了,再写入到数据库中。

缓存更新策略

1.LRU/LFU/FIFO算法剔除,给缓存设置一个maxmemory-policy,缓冲自动选择策略去更新(对于低一致性的,我们采用最大内存更新策略)

2.设置过期时间,对于一些一致性要求不高的,我们可以用这种方法。

3.主动更新,开发控制生命周期,一致性要求比较高的采用这种方法。(可以用一个消息队列,当数据库中内容修改了,通知到消费者去重新查数据库)

   当对一致性有一定要求的时候,我们采用2,3,最大内存更新策略兜底。

下面是主动更新的几种情况:

一个服务调用的过程:

3种策略:

1.先更新数据库,再更新缓存。

 A更新数据库->B更新数据库->B更新缓存->A更新缓存(导致不一致)

2.先删除缓存再更新数据库。

 A写入数据库,先删除缓存->B去查询,缓存没有数据->B去查询数据库读取旧值并更新缓存->A写入数据库(导致不一致)

延时双删策略:

A删除缓存->A写入数据库->睡眠1s再删除缓存

3.先更新再删除

缓存刚好失效->A查询数据库得到旧值->B更新数据库->B删除缓存->A更新缓存(导致不一致)

出现这种的情况很低,写很慢。

 

在实际的项目中,我们采用2,3两种情况,对于并发量比较小的情况,我们可以直接采用 2,但是对于并发量很大的时候,2还是会出现不一致。

对于2在高并发的情况下我们的解决方案是:

对于一个工作线程建一个内存队列,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。(这样就可以保证一致的问题了)。

这里的优化点就是更新数据库同一个字段或者读请求更新缓存都可以直接用后面的去替代前面的,不用重复操作。

待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。

出现的一些问题:

1、读请求长时阻塞

由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。

该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。

另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。

一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。

如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。

这主要还是强一致和性能的一个抉择:(像上面的2加队列这种情况就是一种强一致)

3这种方案就是在并不是很要求强一致的情况下,我们可以短暂的追求性能,在用户读的时候,可能并不是最实时的数据,但是不会出现阻塞这种问题。

具体的情况还是要根据具体的场景来决定

 

缓存粒度控制

选用全量属性,通用性会更好,也便于维护,像user表这种,用全量属性还可以,

但我们选用缓存就需要考虑性能和空间的问题,只保存我们需要的属性就好了(但后期表结构改了,维护性很差)

 

缓存穿透:(直接对存储层操作,失去了缓存层的意义)

        查询一个数据库中不存在的数据,比如商品详情,查询一个不存在的ID,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成过大地压力。

解决方案:

       1.当通过某一个key去查询数据的时候,如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值,比如“NULL”,并设置一个缓存的失效时间,这时在缓存失效之前,所有通过此key的访问都被缓存挡住了。后面如果此key对应的数据在DB中存在时,缓存失效之后,通过此key再去访问数据,就能拿到新的value了。

       2.常见的则是采用布隆过滤器(可以用很小的内存保留很多的数据),将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。(布隆过滤器:实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)

 关于布隆过滤器:

 

缓存雪崩:(缓存失效)

      缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

     1.将系统中key的缓存失效时间均匀地错开,防止统一时间点有大量的key对应的缓存失效;

     2.重新设计缓存的使用方式,当我们通过key去查询数据时,首先查询缓存,如果此时缓存中查询不到,就通过分布式锁进行加锁,取得锁的进程查DB并设置缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回缓存数据或者再次查询DB。

     3.尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上

     4.本地memcache缓存 + hystrix限流&降级,避免MySQL崩掉

     

假如已经崩溃了:也可以利用redis的持久化机制将保存的数据尽快恢复到缓存里。

 

缓存无底洞:

为了满足业务大量加节点,但是性能没提升反而下降。

当客户端增加一个缓存的时候,只需要 mget 一次,但是如果增加到三台缓存,这个时候则需要 mget 三次了(网络通信的时间增加了),每增加一台,客户端都需要做一次新的 mget,给服务器造成性能上的压力。

同时,mget 需要等待最慢的一台机器操作完成才能算是完成了 mget 操作。这还是并行的设计,如果是串行的设计就更加慢了。

通过上面这个实例可以总结出:更多的机器!=更高的性能

但是并不是没办法,一般在优化 IO 的时候可以采用以下几个方法。

  1. 命令的优化。例如慢查下 keys、hgetall bigkey。
  2. 我们需要减少网络通讯的次数。这个优化在实际应用中使用次数是最多的,我们尽量减少通讯次数。
  3. 降低接入成本。比如使用客户端长连接或者连接池、NIO 等等。

 

参考:https://blog.csdn.net/hukaijun/article/details/81010475(讲的非常好)缓存数据库一致问题

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM