缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
举例:
简单来说就是你数据库的id都是1开始然后自增的,那我知道你接口是通过id查询的,我就拿负数去查询,这个时候,会发现缓存里面没这个数据,我又去数据库查也没有,一个请求这样,100个,1000个,10000个呢?你的DB基本上就扛不住了
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
Bloom Filter 布隆过滤器
,把缓存中有的key做哈希存起来,如果是布隆过滤器里面匹配不到的直接返回。
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
- 设置热点数据永远不过期。
- 加互斥锁,保证只有一个线程去查数据库,其他等待一段时间再查缓存。
缓存雪崩
是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期
Redis分布式锁
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。(setnx和expire可以合成一条指令,防止setnx之后执行expire之前进程意外crash或者要重启维护)
1 | set key value [EX seconds] [PX milliseconds] [NX|XX] |
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中有一行配置该配置就是配内存淘汰策略的 ) - 淘汰策略:
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。