Redis缓存
缓存更新方案
低一致性需求:使用Redis自带的内存淘汰机制
高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
- 先写数据库(出现数据不一致性的概率比先更新缓存低),然后再删除缓存(如果采用更新缓存,频繁更新缓存,却未访问,可能带来过多无效的写操作)
- 要确保数据库与缓存操作的原子性
缓存穿透
缓存穿透:访问的数据在Redis缓存和数据库都不存在,这样缓存永远不会生效,这些请求都会打到数据库
常见的解决方案:
缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47private final StringRedisTemplate stringRedisTemplate;
/**
* 普通存储
* @param key 键
* @param value 值
* @param time 过期时间
* @param unit 时间单元
*/
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value),time,unit);
}
/**
* 根据key获取值(缓存穿透解决:缓存空值)
* @param key 缓存键
* @param clazz 数据类型
* @param w 数据库查询条件
* @param function 数据库操作函数
* @param time 数据存入缓存过期时间
* @param unit 时间单元
* @return
* @param <R>
* @param <W>
*/
public <R,W> R get(String key, Class<R> clazz, W w, Function<W,R> function,Long time, TimeUnit unit){
//查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
//缓存命中数据
if (StringUtils.hasText(json)){
return JSON.parseObject(json,clazz);
}
//命中空值
if (json != null && json.equals("")) {
return null;
}
//缓存未命中
//查询数据库
R r = function.apply(w);
//数据库无数据,缓存空值,解决缓存穿透
if (r==null){
set(key,"",10L,TimeUnit.SECONDS);
}else {//数据库有数据,写入缓存
set(key,r,time,unit);
}
return r;
}布隆过滤
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能
缓存雪崩
缓存雪崩:同一时段大量的key同时失效或者Redis服务宕机,导致大量请求到达数据库
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿:某一个高并发访问并且缓存重建业务较复杂的key(热点key)突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击
常见解决方案:
互斥锁(某一个线程获取锁进行缓存重建,其他线程进行重试)
逻辑过期(缓存永不过期,为缓存设置逻辑过期时间,线程访问数据时根据逻辑过期时间判断是否过期,过期获取锁,开启新线程进行重建,未重建完时本线程和其他线程返回旧数据)
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70/**
* 逻辑过期存储(用于热点key)
* @param key 键
* @param value 值
* @param time 逻辑过期时间
* @param unit 时间单元
*/
public void setWithLogicTime(String key,Object value,Long time,TimeUnit unit){
RedisData redisData = new RedisData();
redisData.setLogicTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
redisData.setData(value);
stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(redisData));
}
private ExecutorService executorService= Executors.newFixedThreadPool(5);
/**
* 根据具有逻辑过期key获取数据(缓存击穿解决:逻辑过期)
* @param key 键
* @param clazz 数据类型
* @param w 数据库查询条件
* @param function 数据库操作函数
* @param time 缓存重建key逻辑过期时间
* @param unit 时间单元
* @return
* @param <R>
* @param <W>
*/
public <R,W> R getWithLogicTime(String key,Class<R> clazz,W w,Function<W,R> function,Long time,TimeUnit unit){
//查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
//判断是否逻辑过期
RedisData redisData = JSON.parseObject(json, RedisData.class);
//过期,缓存重建
if (LocalDateTime.now().isAfter(redisData.getLogicTime())) {
//获取互斥锁
boolean lock = getLock("Lock:" + key);
//获取锁成功
if (lock) {
//开启新线程
//查询数据库
executorService.submit(() -> {
//写入缓存
setWithLogicTime(key, function.apply(w), time, unit);
delLock("Lock:" + key);
});
}
}
//逻辑过期和获取锁失败
//返回旧数据
return JSON.parseObject(JSON.toJSONString(redisData.getData()),clazz);
}
/**
* 获取互斥锁
* @param key
* @return
*/
private boolean getLock(String key){
Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return Optional.ofNullable(b).orElse(false);
}
/**
* 释放互斥锁
* @param key
*/
private void delLock(String key){
stringRedisTemplate.delete(key);
}
- 本文作者: zzr
- 本文链接: http://zzruei.github.io/2024/0554e7f04f.html
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!