商户查询缓存
缓存适用于读多写少的场景,变动频率低
根据id查询缓存
1 | /** |
查询店铺类型
更换以下内容:
String key = CACHE_SHOP_TYPE_KEY + UUID.randomUUID().toString(true);shopTypeJSONtypeList = this.list(new LambdaQueryWrapper<ShopType>().orderByAsc(ShopType::getSort));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(typeList),CACHE_SHOP_TYPE_TTL, TimeUnit.MINUTES);
缓存更新策略
内存淘汰
超时剔除
给缓存数据添加TTL,到期后自动删除
主动更新
修改数据库同时,更新缓存
最佳实践
1 | /** |
缓存穿透
客户端请求的数据在 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/**
* 根据id查询商铺数据
*/
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1、从Redis中查询店铺数据
String shopJson = stringRedisTemplate.opsForValue().get(key);
Shop shop = null;
// 2、判断缓存是否命中
if (StrUtil.isNotBlank(shopJson)) {
// 2.1 缓存命中,直接返回店铺数据
shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 2.2 判断缓存中查询的数据是否是空字符串
if (shopJson != null){
// 此时缓存数据为空值
return Result.fail("店铺不存在");
}
shop = this.getById(id);
// 4、判断数据库是否存在店铺数据
if (Objects.isNull(shop)) {
// 4.1 数据库中不存在,缓存空值(解决缓存穿透)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
// 4.2 数据库中存在,重建缓存,并返回店铺数据
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
} -
布隆过滤器
缓存雪崩
在同一时段大量的缓存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// 互斥锁解决缓存击穿
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从Redis中查询店铺数据
String shopJson = stringRedisTemplate.opsForValue().get(key);
Shop shop = null;
// 2.判断缓存是否命中
if (StrUtil.isNotBlank(shopJson)) {
// 3.缓存命中,直接返回店铺数据
return JSONUtil.toBean(shopJson, Shop.class);
}
// 判断缓存中查询的数据是否是空字符串
if (shopJson != null){
return null;
}
// 4.实现缓存重建
String lockKey = LOCK_SHOP_KEY + id;
try {
boolean isLock = tryLock(lockKey);
if (!isLock) {
// 获取锁失败,已有线程在重建缓存,则休眠重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 获取锁成功,根据id查询数据库
shop = this.getById(id);
// 模拟重建的延时
Thread.sleep(200);
// 5. 判断数据库是否存在店铺数据
if (Objects.isNull(shop)) {
// 数据库中不存在,缓存空值(解决缓存穿透)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6. 数据库中存在,重建缓存,并返回店铺数据
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7. 释放互斥锁
unlock(lockKey);
}
return shop;
}
/**
* 获取锁
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 拆箱要判空,防止NPE
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
} -
逻辑过期
-
不设置TTL,但在Value中加一个过期时间字段,在获取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
71
public class RedisData {
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 缓存数据
*/
private Object data;
}
// 逻辑过期解决缓存击穿
public Shop queryWithLogicExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1、从Redis中查询店铺数据
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2、判断缓存是否命中
if (StrUtil.isBlank(shopJson)) {
// 3.未命中,直接返回
return null;
}
// 4.命中, 判断过期时间, 反序列化
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return shop;
}
// 5.2.已过期,需要缓存重建
// 6.1. 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2. 获取锁成功,开启独立线程,实现缓存重建
if (isLock) {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
});
}
// 6.3. 返回过期的店铺信息
return shop;
}
// 线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 预热:将数据保存到缓存中
* 重建缓存
*/
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
// 从数据库中查询店铺数据
Shop shop = this.getById(id);
Thread.sleep(200);
// 封装逻辑过期数据
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 将逻辑过期数据存入Redis中
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}先进行数据预热,热点数据加载到缓存中
编写一个
CommandLineRunner继承类,然后SpringBoot程序启动时就执行启动的run方法,实现数据预热,或者像下面一样编写一个测试类1
2
3
4
5
6
7/**
* 预热店铺数据
*/
public void testSaveShop(){
shopService.saveShop2Redis(1L, 10L);
}
-
缓存工具类的封装
利用泛型抽取出一个工具类,编写几个通用的方法,解决缓存穿透、缓存击穿,只需要调用工具类的方法解决。
使用工具类:
1 |
|
ShopServiceImpl的代码:
1 | /** |