Redis中的缓存失效
# Redis中的缓存失效
# 缓存击穿
Redis中的缓存击穿是指,在高并发的情况下,如果缓存中不存在某个请求的Key,并发请求会直接打到数据库,造成数据库压力过大甚至崩溃的现象。
这种情况通常发生在某个热点Key的缓存在失效的瞬间,大量的并发请求由于没有在缓存中找到对应的Value而直接打到了数据库上。
简单总结:Redis缓存中没有对应数据,但数据库还有。
# 如何解决
为了避免这种现象的发生,可以采用双重检测锁、互斥锁等技术手段,让每次只有一个请求能够直接打到数据库,其他的请求则在获取到锁之前处于等待状态,这样就可以有效缓解数据库的压力。
- 双重检测锁
“双重检测锁”机制:从数据库查询数据方法外加锁,锁内再次判断缓存是否有数据。
//使用"双重检测锁"机制查询redis
//第一次查询redis
if(查询结果 是否为空){
// code .....
// 如果存在,直接返回,无需查询数据库
}else{
synchronized(){
// 再次查询redis 如果没有才继续往数据库查询
}
}
2
3
4
5
6
7
8
9
10
因为加了锁,所以请求会一个一个的进行,在第一个查询为空后,接着往mysql等数据库后,会将对应的redis-key给重新设置,便不会发生大量请求同时查询为空的情况,不会发生缓存击穿。
- 对数据提前预热
或者提前对热点数据进行设置,预先存在Redis中,或者适当延长Redis中对应key的过期时间。
# 缓存穿透
缓存穿透:客户端请求的数据在缓存中和数据库中都不存在,这样缓存就永远不会生效,同一时间大量请求请求都会打到数据库上,造成数据库压力过大甚至崩溃的现象。
这种情况通常发生在缓存失效之后,客户端仍然不断发起请求,而且请求的数据在数据库中也不存在。
Redis缓存中跟数据库中都没有对应的数据。
# 如何解决
为了避免这种现象的发生,可以先判断一下客户端请求的数据是否存在,如果不存在,则直接返回空结果,避免打到数据库上;或者进行类似用户权限的拦截,将一些请求进行拦截,不允许访问。
或者使用布隆过滤器
# 布隆过滤器
# 定义
布隆过滤器是1970年由一个叫布隆的小伙子提出来的。它其实就是一个很长的二进制向量,可能还要加上一个(或多个)哈希函数。
主要由两个部分组成:一个位数组(Bit Array)和一组哈希函数。
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它通过牺牲一定的准确性
来换取空间和时间的高效性。
# 添加与查询过程
以下是一个简要的示例图:
初始状态:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
插入 "apple":
- 哈希函数1: 索引2 -> [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
- 哈希函数2: 索引5 -> [0, 0, 1, 0, 0, 1, 0, 0, 0, 0]
- 哈希函数3: 索引8 -> [0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
插入 "banana":
- 哈希函数1: 索引3 -> [0, 0, 1, 1, 0, 1, 0, 0, 1, 0]
- 哈希函数2: 索引6 -> [0, 0, 1, 1, 0, 1, 1, 0, 1, 0]
- 哈希函数3: 索引9 -> [0, 0, 1, 1, 0, 1, 1, 0, 1, 1]
最终状态:[0, 0, 1, 1, 0, 1, 1, 0, 1, 1]
2
3
4
5
6
7
8
9
10
11
12
13
上述就是它的一个简要添加元素过程,
- 将要插入的元素通过每个哈希函数计算出一个索引值。
- 将这些索引值对应的位数组中的位设置为1。
而查询操作也类似:
- 将要查询的元素通过每个哈希函数计算出一个索引值。
- 检查这些索引值对应的位数组中的位是否都为1。
- 如果所有位都是1,则认为该元素可能在集合中;如果有任何一个位为0,则可以确定该元素不在集合中。
# 使用与注意事项
布隆过滤器的查询效率是很高的,因为它是通过哈希函数进行运算,再在一个有序的位数组上查找。
但是,请注意,布隆过滤器可能会出现误判,即判断一个不在集合中的元素为可能存在。因为存在哈希碰撞,不同的值它们的哈希值可能相同。所以,布隆过滤器采用了多个hash函数来计算,这样一定程度上可以避免hash碰撞的问题,当然因为要存储多个hash值,所以要想准确率高,位数组的长度也要足够长才行。
不过,布隆过滤器不会出现漏判,即不会判断一个在集合中的元素为不存在。
关于删除:
说了添加与查找,为什么没说删除呢?是布隆过滤器不支持吗?是的,布隆过滤器不支持删除元素。
原因:因为存在哈希碰撞,所以,可能出现同一个位置,代表多个值的哈希函数的值。如果删除了其中一个,那么其余的值将也会被删掉。
# 代码实现
在代码中,如何实现呢?可以借用Google公司出的Guava,里面有具体的实现。同时它还可以自定义出错率。
- 使用Guava中的布隆过滤器
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
2
3
4
5
6
使用布隆过滤器
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterTest {
/** 预计插入的数据 */
private static Integer expectedInsertions = 10000000;
/** 误判率 */
private static Double fpp = 0.01;
/** 布隆过滤器 */
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), expectedInsertions, fpp);
public static void main(String[] args) {
// 插入 1千万数据
for (int i = 0; i < expectedInsertions; i++) {
bloomFilter.put(i);
}
// 从10000000开始,用1千万数据测试误判率
int count = 0;
for (int i = expectedInsertions; i < expectedInsertions *2; i++) {
if (bloomFilter.mightContain(i)) {
count++;
}
}
System.out.println("一共误判了:" + count);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 使用Java集成Redis使用布隆过滤器
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.0</version>
</dependency>
2
3
4
5
6
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedisBloomFilterTest {
/** 预计插入的数据 */
private static Integer expectedInsertions = 10000;
/** 误判率 */
private static Double fpp = 0.01;
public static void main(String[] args) {
// Redis连接配置,无密码
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.211.108:6379");
// config.useSingleServer().setPassword("123456");
// 初始化布隆过滤器
RedissonClient client = Redisson.create(config);
RBloomFilter<Object> bloomFilter = client.getBloomFilter("user");
bloomFilter.tryInit(expectedInsertions, fpp);
// 布隆过滤器增加元素
for (Integer i = 0; i < expectedInsertions; i++) {
bloomFilter.add(i);
}
// 统计元素
int count = 0;
for (int i = expectedInsertions; i < expectedInsertions*2; i++) {
if (bloomFilter.contains(i)) {
count++;
}
}
System.out.println("误判次数" + count);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 缓存雪崩
Redis中的缓存雪崩是指在某一时刻,大量的缓存同时过期失效,导致大量的请求直接打到数据库上,造成数据库压力剧增,甚至可能引发整个系统的崩溃。这种情况通常发生在某些热点Key的缓存在同一时刻失效,或者是大量Key在同一时刻过期失效。
缓存雪崩跟缓存击穿有些类似,不过雪崩是大量缓存同时失效,而击穿则是少数几个失效,而大量请求同时请求它们。
导致雪崩的场景通常有两个:
- 大量热点key同时过期
- 缓存服务出现故障或者宕机
# 如何解决
为了避免这种现象的发生,可以采取以下几种措施:
- 设置合理的缓存失效时间:将缓存中的数据设置成不同的过期时间,或者设置一个较长的缓存失效时间,从而减少缓存失效的数量,从而减少缓存雪崩的概率。
- 数据预热:可以在缓存失效之前,提前将热点数据加载到缓存中,从而减少缓存失效的数量,减少缓存雪崩的概率。
- 分布式锁:在缓存失效的瞬间,先使用分布式锁锁定该Key,然后只允许一个请求能够打到数据库上,其他的请求则等待锁释放后才能继续执行。
- 容错降级:当发生缓存雪崩时,可以考虑开启容错降级模式,限制请求量,降低数据库压力。