缓存问题&更新策略
# 缓存问题&更新策略
- 保证数据库和缓存的原子性
- 写操作:先写数据库,然后再删除缓存(先删缓存容易出现雪崩)
- 读操作:先查缓存,没有则查数据库,然后写进缓存,并设置超时时间。
# 缓存穿透
缓存穿透:客户端请求的数据在缓存中和数据库中都不存在。缓存永远不会生效,最终打到数据库。
①缓存空对象:为了不让请求再次打到数据库,当数据库查询没有结果时,向redis中缓存一个null。
缺点:内存中缓存了过多的垃圾(可以设置TTL)
stringRedisTemplate.opsForValue().set(id.toString(), "",2,TimeUnit.MINUTES);
1
②布隆过滤器:在客户端和Redis之间添加一层布隆过滤器,请求过来时,布隆过滤器会告诉客户端数据在redis还是数据库,如果都不在则直接返回。布隆过滤器说不在,那么数据肯定不在DB;而如果布隆过滤器说在,那么数据可能在DB,出现误判。
布隆过滤器原理可以认为是一个值只为0或1的数组,添加数据时,比如说我要添加A,就会同通过一个hash散列映射为一个0101的字符串,写入数组的时候只需要在字符串对应为1的位置记为1即可(0位置是否为0不需要关注)。查询数据时,假设我要查询B,就会先通过hash散列映射为一个字符串,然后在布隆过滤器数组中查看字符串所有对应为1的位置是否都为1,如果存在不为1的说明该数据不在集合中。这种方法只能加数据不能减数据,也会存在误判,在于如果映射的字符串为0100,那么就会出现误判。因此误判率高低取决于hash散列的复杂程度,以及数组的长度。
# 缓存雪崩
缓存雪崩:Redis大量的缓存key同时失效过期,或者redis宕机,导致大量请求到达数据库。
①给不同key的TTL添加随机值
②Redis集群,服务降级限流
# 缓存击穿
缓存击穿:热点key问题。高并发访问的key失效了,并且这个热点key请求的时间较长业务复杂,在这段时间内无数的请求都会瞬间达到数据库。
①互斥锁:线程需要等待,保证一致性。使用setnx实现互斥锁,释放锁时则删除key
- 查redis:①有数据直接返回②有空数据直接返回(解决缓存穿透)
- 申请锁:①如果没有获得则重新进入函数②如果获得锁,再检查一次redis有数据直接返回(二次检查),redis没有则查数据库。
- 释放锁
public Shop queryWithmutex(Long id) {
//1.查redis
//2.申请锁查数据库
String lockkey = "lock" + id;
Shop shop = null;
try {
boolean b = tryLock(lockkey);
if (!b) {
Thread.sleep(50);
return queryWithmutex(id);
}
//二次查询数据库
String res = stringRedisTemplate.opsForValue().get(id.toString());
if (StrUtil.isNotBlank(res)) {
return JSONUtil.toBean(res, Shop.class);
}
shop = getById(id);
Thread.sleep(200);
if (shop == null) {
//解决缓存穿透
stringRedisTemplate.opsForValue().set(id.toString(), "",2,TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set(id.toString(), JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unlock(lockkey);
}
return shop;
}
1
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
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
②逻辑过期:线程无需等待,性能好。不保证一致性。(互斥锁+线程池)

# 封装工具类代码复用
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
1
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
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
编辑 (opens new window)
上次更新: 2023/12/15, 15:49:57