【自用】Redis应用场景之缓存
缓存
缓存穿透(cache penetration)
缓存穿透(大概率是遭到恶意攻击):查询一个不存在的数据,mysql查不到数据也就不会直接写入缓存,这就会导致每次请求都查数据库。有可能导致数据库压力过大宕机。
解决方案①:缓存空数据
缓存空数据{key:1,value:null},查询返回的数据为空,仍把这个空结果进行缓存。
优点:简单方便
缺点:如果有大量的getById请求不存在的数据,内存压力就会很大;另外还可能发生缓存和DB数据不一致的问题(redis缓存了空数据,但是后来DB真的更新上这个数据了,redis却还是null)。
解决方案②:布隆过滤器
布隆过滤器的作用就是检索一个元素是否在一个集合当中,从而拦截不存在的数据。布隆过滤器先判断请求的值是否存在:不存在直接拦截返回,存在的话才走redis或DB。前提:缓存预热的时候,会先预热布隆过滤器。
底层实现:先取一个比较大的数组(bitmap aka 位图),里面存放二进制0或1。一开始都是0,每加入一个新key / 尝试查询一个key时,就进行3次hash运算,模于数组长度找到数据的下标,把那个单元改为1。如此就表示该key存在。
它的实现依赖于bitmap。
- bitmap(位图):相当于是一个以 bit 为单位的数组,数组中每个单元只能存二进制数。
优点:内存占用较少,没有多余的key
缺点:实现相对复杂一些;而且存在误判的可能性。
查到不一定存在,查不到一定不存在。如下图所示。
误判率:数组越小,误判率越大;数组越大,误判率越低,但是同时内存消耗也会越大。
具体实现中,可以用Redisson、Guava等实现方案来设置误判率,误判率设置合适就行,比如5%以内,不至于在高并发的时候压倒数据库。
缓存击穿(cache breakdown)
缓存击穿:给某一个key设置了过期时间,当key过期时,恰好这个时间点对这个key有大量的并发请求进来。这些请求一般都是会从后端DB加载数据并回设到缓存的,但是这时的大并发的请求可能会瞬间把DB压垮。(为什么缓存这么慢呢?因为有的情况下,外面存入缓存的数据可能是多个表汇总的结果,可能需要分组统计。)
解决方案①:互斥锁 (aka 分布式锁) - 强一致,性能差
当缓存失效查询未命中时,不立即去load db,而是先使用比如Redis的setnx去设置一个互斥锁,当操作成功时再进行load db操作并回设缓存,不成功就一直重试get缓存的方法。
解决方案②:逻辑过期 - 高可用,性能优,不能保证数据绝对一致
大概思路是:
对redis中热点的key不设置过期时间,而是设置一个过期时间字段一块存入缓存。。。,当前key设置过期时间。
当查询的时候,从redis取出数据后判断时间是否过期
如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,但这个数据并不是最新的。
两种方案各有利弊
如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,另外也可能产生死锁的问题。(如跟钱相关的业务数据)
如果选择key的逻辑删除,则优先考虑的是高可用性,性能比较高,但是数据同步就做不到强一致了。(如侧重于用户体验的业务)
缓存雪崩(cache avalanche)
缓存雪崩是指在同一时段内大量的缓存key同时失效,或者Redis服务宕机,导致大量请求全部到达DB,DB瞬间访问压力太大,导致“雪崩”。
与缓存击穿的区别:雪崩是很多key;而击穿是某一个key缓存。
情况一:在同一时段内大量的缓存key同时失效(也就是设置缓存时采用了相同的过期时间)
(解决方案①)解决起来比较简单,只需给不同key的TTL添加随机值。
情况二:Redis服务宕机
(解决方案②)为了预防这种问题,我们可以通过搭建Redis集群来提高服务的可用性。比如:哨兵模式、集群模式。
降级限流策略
(解决方案③)在设计这个业务系统时,可以给缓存业务添加降级限流策略。比较常见的限流设置可以在nginx或spring cloud (微服务) 的gateway网关中去设置限流规则。
- 可作为系统的保底策略,使用于:穿透、击穿、雪崩。
多级缓存
(解决方案④)可以给业务添加多级缓存,可以预防大量的key过期。可以使用Guava或Caffeine,让它们作为一级缓存,redis作为二级缓存。