裸泳的猪

沾沾自喜其实最可悲

0%

redis使用之缓存问题及解决方案

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

举例:
简单来说就是你数据库的id都是1开始然后自增的,那我知道你接口是通过id查询的,我就拿负数去查询,这个时候,会发现缓存里面没这个数据,我又去数据库查也没有,一个请求这样,100个,1000个,10000个呢?你的DB基本上就扛不住了

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

  2. Bloom Filter 布隆过滤器

,把缓存中有的key做哈希存起来,如果是布隆过滤器里面匹配不到的直接返回。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:

  1. 设置热点数据永远不过期。
  2. 加互斥锁,保证只有一个线程去查数据库,其他等待一段时间再查缓存。

缓存雪崩

是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期

Redis分布式锁

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。(setnx和expire可以合成一条指令,防止setnx之后执行expire之前进程意外crash或者要重启维护)

1
2
3
4
5
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)

redis设置的锁过期时间到了,但业务并没有执行完成,怎么办?

redisson这个客户端工具,内部有一个监控锁的看门狗,默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒。

双写一致性问题

两个请求同时修改同一个数据,缓存和数据库在双写场景下,一致性是如何保证。

  • 严格要求 “缓存+数据库” 必须保持一致性 : 读请求和写请求串行化

  • Cache Aside Pattern(旁路缓存方案)

    最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。

    读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

    更新的时候,先更新数据库,然后再删除缓存
    注:(区别于更新缓存,如果是更新,在1和2两个并发写发生时,由于无法保证时序,此时不管先操作缓存还是先操作数据库,都可能出现:(1)请求1先操作数据库,请求2后操作数据库(2)请求2先set了缓存,请求1后set了缓存导致,数据库与缓存之间的数据不一致。)

延时双删

上面这种方案,是先更新数据库,再删除缓存,但是改了库,清理缓存前,有部分事务还是会拿到旧缓存,这样如果再更新了缓存之后就还是不对的。可以采用延时双删策略。

在更新数据库前,清理缓存,再执行更新操作,然后第二次清空缓存之前,多延时一会儿,等B更新缓存结束了,再删除缓存,这样就缓存就不存在了,其他事务查询到的为新缓存。

延时是确保 修改数据库 -> 清空缓存前,其他事务的更改缓存操作已经执行完。

采用延时删最后一次缓存,但这其中难免还是会大量的查询到旧缓存数据的。

这个时候可以采用通过加锁来解决,一次性不让太多的线程都来请求,另外从图上看,我们可以尽量缩短第一次删除缓存更新数据库的时间差。

redis的过期策略以及内存淘汰机制

分析:比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?

回答: redis采用的是定期删除+惰性删除 + 淘汰策略。

定期删除+惰性删除 + 淘汰策略是如何工作的呢?

纯定时删除,十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略。

  • 定期删除:redis默认每个100ms随机抽取进行检查是否有过期的key,有过期key则删除。

    (如果只采用定期删除策略,会导致很多key到时间没有删除。所以加入惰性删除 )

  • 惰性删除:也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期,如果过期了此时就会删除。

    (如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。)
    在redis.conf中有一行配置该配置就是配内存淘汰策略的 )

  • 淘汰策略:
    1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
    2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
    3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
    4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
    5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
    6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
-------------本文结束感谢您的阅读-------------