Redis实践-商户查询缓存

Redis实践-商户查询缓存

1.项目背景

承接上一篇文章,我们还是以黑马点评作为项目背景,看一下Redis缓存在项目中的真实应用,我们看一眼相关的表:

image-20240106161850662

Redis作为缓存服务使用时,提供了高性能、高可用性和易扩展性的解决方案,这是因为Redis的数据基于内存存储访问,速度非常快,还支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,这种多样性使得它可以适应各种类型的缓存需求,同时允许为存储的每个键设置生存时间(TTL),过了这个时间键会被自动删除,这是管理缓存数据,特别是对于有时间敏感性的数据非常有用。除此以外,Redis支持数据的自动分片和集群部署,使得它能够处理更大规模的数据集和客户端连接,支持主从复制,提供数据的热备份,以及故障转移和灾难恢复能力。我们以查询商铺信息为例,看看Redis缓存如何使用:

image-20240106162114270

2.缓存更新策略

image-20240106162750371

对于高一致性需求来说,主动更新策略是逃不掉的,常见的主动更新策略有三种:Cache Aside PatternRead/Write Through PatternWrite Behind Caching Pattern,后两种策略实现和管理复杂性较大,我们这里还是使用第一种策略,即更新数据库的同时更新缓存

image-20240106163232883

image-20240106164507942

image-20240106165717958

image-20240106170121317

3.缓存穿透

image-20240106171225725

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public Result updateShop(Shop shop) {
if (shop.getId() == null) {
return Result.fail("商铺ID不能为空!");
}
// 更新数据库
updateById(shop);
// 删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());
return Result.ok();
}

image-20240106171603637

image-20240106171700943

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Result queryById(Long id) {
// 先从Redis中获取商铺缓存信息
String jsonShop = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 缓存击中,直接返回
if (StrUtil.isNotBlank(jsonShop)) {
return Result.ok(JSONUtil.toBean(jsonShop, Shop.class));
}
// 缓存击中空对象
if (jsonShop != null) {
return Result.fail("商铺不存在!");
}
// 缓存未命中,查询数据库
Shop shop = getById(id);
if (shop == null) {
// 存在缓存穿透风险,选择缓存空对象方案,设置较短的TTL
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("商铺不存在!");
}
// 商铺信息缓存写回Redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}

布隆过滤器Redis实现

image-20240106171957360

4.缓存雪崩

image-20240106172036395

缓存雪崩的解决方案涉及到Redis集群部署和多级缓存,这些内容会在后面的Redis高级篇中学习。

5.热点Key

image-20240106172149508

image-20240106172900488

image-20240106172949248

image-20240106173039675

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
// 热点Key问题解决方案一:互斥锁,性能略差,数据一致性较强
public Result queryByIdWithMutex(Long id) {
String jsonShop = null;
while (true) {
// 双重检查,避免多次缓存重建
jsonShop = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 缓存击中,直接返回
if (StrUtil.isNotBlank(jsonShop)) {
return Result.ok(JSONUtil.toBean(jsonShop, Shop.class));
}
// 缓存击中空对象
if (jsonShop != null) {
return Result.fail("商铺不存在!");
}
try {
// 存在热点Key问题,利用Redis加简单的分布式锁后再进行缓存重建(对于复杂业务可能耗时较长)
if (!tryLock(RedisConstants.LOCK_SHOP_KEY + id)) {
TimeUnit.SECONDS.sleep(1);
continue;
}
// 缓存未命中,查询数据库
Shop shop = getById(id);
if (shop == null) {
// 存在缓存穿透风险,选择缓存空对象方案
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("商铺不存在!");
}
// 商铺信息缓存写回Redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unlock(RedisConstants.LOCK_SHOP_KEY + id);
}
}
}

private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
stringRedisTemplate.delete(key);
}

image-20240106173657261

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
72
73
private static final ExecutorService CACHE_BUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public static final Long CACHE_SHOP_LOGIC_TTL = 10L;
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}

// 热点Key问题解决方案二:逻辑过期,性能较好,数据一致性较差
public Result queryByIdWithLogicExpire(Long id) {
// 先从Redis中获取商铺缓存信息
String jsonRedisData = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 缓存击中,判断商铺信息是否逻辑过期
if (StrUtil.isNotBlank(jsonRedisData)) {
RedisData data = JSONUtil.toBean(jsonRedisData, RedisData.class);
// 商铺信息没有逻辑过期,直接返回
if (data.getExpireTime().isAfter(LocalDateTime.now())) {
return Result.ok(JSONUtil.toBean((JSONObject) data.getData(), Shop.class));
}
try {
// 商铺信息逻辑过期,异步线程完成缓存重建
if (tryLock(RedisConstants.LOCK_SHOP_KEY + id)) {
// 双重检查,避免大量重复的缓存重建
jsonRedisData = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(jsonRedisData)) {
data = JSONUtil.toBean(jsonRedisData, RedisData.class);
if (data.getExpireTime().isAfter(LocalDateTime.now())) {
return Result.ok(JSONUtil.toBean((JSONObject) data.getData(), Shop.class));
}
}
// 缓存击中空对象
if (jsonRedisData.equals("")) {
return Result.fail("商铺不存在!");
}
CACHE_BUILD_EXECUTOR.submit(() -> {
saveShop2Redis(id, RedisConstants.CACHE_SHOP_LOGIC_TTL);
});
}
} finally {
unlock(RedisConstants.LOCK_SHOP_KEY + id);
}
// 返回过期的商铺信息
return Result.ok(JSONUtil.toBean((JSONObject) data.getData(), Shop.class));
}
// 缓存击中空对象
if (jsonRedisData != null) {
return Result.fail("商铺不存在!");
}
// 缓存未命中,进行缓存重建
Shop shop = saveShop2Redis(id, RedisConstants.CACHE_SHOP_LOGIC_TTL);
if (shop == null) {
return Result.fail("商铺不存在!");
}
return Result.ok(shop);
}

// 缓存重建过程
public Shop saveShop2Redis(Long id, Long expireSecond) {
log.info("缓存重建...");
// 查询数据库
Shop shop = getById(id);
if (shop == null) {
// 存在缓存穿透风险,选择缓存空对象方案
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return shop;
}
// 设置逻辑过期时间,商铺信息缓存写回Redis
RedisData data = new RedisData();
data.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond));
data.setData(shop);
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(data));
// 返回商铺对象
return shop;
}