Redis缓存的三大问题
概述
使用Redis
作为缓存时,极大地提升了应用程序的性能和效率,特别是数据查询方面,但是也带来了一些问题,比如典型的缓存穿透
、缓存击穿
、缓存雪崩
。
1. 缓存穿透
缓存穿透
是指请求查询的信息在缓存和数据库中都不存在,始终会访问数据库进行查询,这样会对数据库的访问造成很大的压力,缓存也失去了意义。
举个例子,用户查询一个 id = -1
的商品信息,一般数据库id
值都是从 1 开始自增,很明显这条信息是不在数据库中的,当查询结果为空时,缓存中也不会增加对应数据,之后会一直向数据库查询。
解决方案
一般我们可以想到从缓存开始出发,当用户查询一个当前数据库不存在的信息时,我们将一个空对象返回给用户,并把它缓存起来,之后再次查询相同信息时直接从缓存中返回。
没错,这是一个解决方案,也就是我们常说的缓存空对象
,Redis
也为我们提供了一种解决方案,那就是布隆过滤器
。
那接下来,先解释下这两种方案:
1)缓存空对象
缓存空对象是指一个请求发送过来,如果此时缓存中和数据库都不存在这个请求所要查询的相关信息,那么数据库就会返回一个空对象,并将这个空对象和请求关联起来存到缓存中,当下次还是这个请求过来的时候,这时缓存就会命中,就直接从缓存中返回这个空对象,这样可以减少访问数据库的压力,提高当前数据库的访问性能,流程图:
看上去缓存穿透的问题已经完美解决了,但实际上还并不妥当。随着时间推移,缓存中的空对象会越来越多,这样一来不仅会占用许多的内存空间,还会浪费许多资源。此时我们可以在设置空对象时,顺便设置一个过期时间,时间一到就清除缓存中的这个空对象:
setex key seconds valule:设置键值对的同时指定过期时间(s)
在Java
中直接调用API
操作即可:
redisCache.put(Integer.toString(id), null, 60) //过期时间为 60s
2)布隆过滤器
布隆过滤器
是一种空间效率高的概率型数据结构,它专门用来检测集合中是否存在特定的元素,运行速度快。我们可以将数据库中所有的查询条件,放入布隆过滤器
中,当一个查询请求过来时,先经过布隆过滤器
进行过滤,如果判断请求查询值存在,则继续查;如果判断请求查询不存在,直接返回。
布隆过滤器的特点:
- 一个非常大的二进制位数组(数组中只存在 0 和 1)
- 拥有若干个哈希函数(Hash Function)
- 在空间效率和查询效率都非常高
- 布隆过滤器不会提供删除方法,在代码维护上比较困难。
每个布隆过滤器对应到 Redis
的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash
函数。所谓无偏就是能够把元素的 hash
值算得比较均匀。
向布隆过滤器中添加 key
时,会使用多个 hash
函数对 key
进行 hash
算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash
函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add
操作。( 每一个 key
都通过若干的hash
函数映射到一个巨大位数组上,映射成功后,会在把位数组上对应的位置改为1。)
为什么布隆过滤器会存在误判率?
其实它会误判是如下这个情况:
当 key1
和 key2
映射到位数组上的位置为 1 时,假设这时候来了个 key3
,要查询是不是在里面,恰好 key3
对应位置也映射到了这之间,那么布隆过滤器会认为它是存在的,这时候就会产生误判(因为明明 key3
是不在的)。
如何提高布隆过滤器的准确率
要提高布隆过滤器的准确率,就要说到影响它的三个重要因素:
- 哈希函数的好坏
- 存储空间大小
- 哈希函数个数
hash
函数的设计也是一个十分重要的问题,对于好的hash
函数能大大降低布隆过滤器的误判率。
同时,对于一个布隆过滤器来说,如果其位数组越大的话,那么每个key
通过hash
函数映射的位置会变得稀疏许多,不会那么紧凑,有利于提高布隆过滤器的准确率。同时,对于一个布隆过滤器来说,如果key
通过许多hash
函数映射,那么在位数组上就会有许多位置有标志,这样当用户查询的时候,在通过布隆过滤器来找的时候,误判率也会相应降低。
对于其内部原理,有兴趣的同学可以看看关于布隆过滤的数学知识,里面有关于它的设计算法和数学知识。
2. 缓存击穿
缓存击穿是指有某个经常被用户查询的热门key
,在缓存过期时间到期时突然被大量请求访问,或者一个平时无人问津的冷门key
突然迎来大量请求,这时会导致大并发请求直接穿透缓存,请求数据库,瞬间对数据库的访问压力增大。
归纳起来:造成缓存击穿的原因有两个。
(1)一个冷门key
,突然被大量用户请求访问。
(2)一个热门key
,在缓存中时间恰好过期,这时有大量用户来进行访问。
对于缓存击穿的问题:我们常用的解决方案是加锁。当key
过期或比较冷门导致缓存中无相关数据,要查询数据库的时候加上一把锁,这时只能让第一个请求进行查询数据库,然后把从数据库中查询到的值存储到缓存中,对于剩下的相同的key
,可以直接从缓存中获取即可。
如果我们是在单机环境下:直接使用常用的锁即可(如:Lock
、Synchronized
等),在分布式环境下我们可以使用分布式锁,如:基于数据库、基于Redis
或者zookeeper
的分布式锁。
3. 缓存雪崩
缓存雪崩
是指在某一个时间段内,缓存集中过期失效,如果这个时间段内有大量请求,而查询数据量巨大,所有的请求都会达到存储层,存储层的调用量会暴增,引起数据库压力过大甚至宕机。
原因:
Redis
突然宕机- 大部分数据失效
举个例子理解下吧:
比如我们基本上都经历过购物狂欢节,假设商家举办 23:00-24:00 商品打折促销活动。程序员在设计的时候,在 23:00 把商家打折的商品放到缓存中,并通过redis
的expire
设置了过期时间为1小时。这个时间段许多用户访问这些商品信息、购买等等。但是刚好到了24:00点的时候,恰好还有许多用户在访问这些商品,这时候对这些商品的访问都会落到数据库上,导致数据库要抗住巨大的压力,稍有不慎会导致,数据库直接宕机。
当缓存没有失效的时候是这样的:
当缓存失效的时候却是这样的:
对于缓存雪崩有以下解决方案:
(1)redis高可用
搭建redis
集群,多增加几台redis
实例(一主多从或者多主多从),这样一台挂掉之后其他的还可以继续工作。
(2)限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,对某个key
只允许一个线程查询数据和写缓存,其他线程等待。
(3)数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key
。
(4)不同的过期时间
设置不同的过期时间,让缓存失效的时间点尽量均匀。