Redis实战篇–商户查询缓存
缓存
缓存就是数据交换的缓冲区(称作Cache [ka]),是存数据的临时地方,一般读写性能较高。
缓存的作用
- 降低后端负载
- 提高读写效率
- 降低响应时间
缓存的成本
- 数据一致性成本
- 代码维护成本
- 运维成本
添加redis缓存
将店铺信息存入redis中
主要流程
业务代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public Result getShopById(Long id) { String key = CACHE_SHOP_KEY + id; //1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.是否存在 if(!StringUtil.isNullOrEmpty(shopJson)){ //3.存在 返回商铺信息 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //4.不存在根据id在数据库中查询商铺信息 Shop shop = getById(id); //5.不存在 返回错误信息 if (shop == null) { return Result.fail("店铺不存在"); } //6.存在 将商铺信息写入redis中 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); //7.将商铺信息返回 return Result.ok(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
26public List<ShopType> queryShopList() { //1.在redis中查询是否存在缓存 String key = RedisConstants.CACHE_SHOPlIST_KEY; List<String> shopList = stringRedisTemplate.opsForList().range(key, 0, -1); //2.缓存命中 直接返回shopList if (shopList.size() > 0 ) { List<ShopType> shopTypeList = shopList.stream().map((list) -> { ShopType shopType = JSONUtil.toBean(list, ShopType.class); return shopType; }).sorted(Comparator.comparing(ShopType::getSort)) .collect(Collectors.toList()); return shopTypeList; }else { //3.缓存未命中 则在数据库中查询shopList List<ShopType> typeList = query().orderByAsc("sort").list(); List<String> strings = typeList.stream().map((shop) -> { return JSONUtil.toJsonPrettyStr(shop); }).collect(Collectors.toList()); if(typeList.size() > 0){ //5.存在 写入redis中 stringRedisTemplate.opsForList().leftPushAll(key,strings); }//6.返回list return typeList; } }
使用缓存与未使用缓存的效果对比
缓存更新策略
缓存更新策略主要有以下三种
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用redus的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
业务场景:
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
主动更新策略
Cache Aside Pattern
由缓存的调用者,在更新数据库的同时更新缓存。
Read/Write Through Pattern
缓存与数据库整合为一个服务由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
Write Behind Caching Pattern
调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。
操作缓存和数据库时有三个问题需要考虑
1,删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
2.如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
先操作缓存还是先操作数据库?
- 先删除缓存,再操作数据库
- 先操作数据库,再删除缓存
缓存更新策略的最佳实践方案
1.低一致性需求:使用Redis自带的内存淘汰机制
2.高一致性需求:主动更新,并以超时剔除作为兜底方案
-
读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
-
写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
案例
给查询商铺的缓存添加超时剔除和主动更新的策略
修改ShopController中的业务逻辑,满足下面的需求:
① 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
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
26public Result getShopById(Long id) { String key = CACHE_SHOP_KEY + id; //1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.是否存在 if(!StringUtil.isNullOrEmpty(shopJson)){ //3.存在 返回商铺信息 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //判断命中的是否是空值 if(shopJson != null){ return Result.fail("店铺不存在2"); } //4.不存在根据id在数据库中查询商铺信息 Shop shop = getById(id); //5.不存在 返回错误信息 if (shop == null) { return Result.fail("店铺不存在1"); } //6.存在 将商铺信息写入redis中 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); //7.将商铺信息返回 return Result.ok(shop); }
② 根据id修改店铺时,先修改数据库,再删除缓存.
1
2
3
4
5
6
7
8
9
10
11
12
13@Transactional public Result update(Shop shop) { Long id = shop.getId(); if(id == null){ return Result.fail("店铺Id不能为空"); } //1.更新数据库 updateById(shop); //2.删除缓存 stringRedisTemplate.delete(CACHE_SHOP_KEY+id); return Result.ok(); }
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
实现流程图
布隆过滤
- 优点:内存占用较少,没有多余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
27public Result getShopById(Long id) { String key = CACHE_SHOP_KEY + id; //1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.是否存在 if(!StringUtil.isNullOrEmpty(shopJson)){ //3.存在 返回商铺信息 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //判断命中的是否是空值 if(shopJson != null){ return Result.fail("店铺不存在2"); } //4.不存在根据id在数据库中查询商铺信息 Shop shop = getById(id); //5.不存在 返回错误信息 if (shop == null) { stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("店铺不存在1"); } //6.存在 将商铺信息写入redis中 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); //7.将商铺信息返回 return Result.ok(shop); }
总结
缓存穿透产生的原因是什么?
- 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存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
71
72
73
74
75
76
77
78
79
80
81/** * 缓存击穿 * * @param id * @return */ public Shop queryWithMutex(Long id) { String key = CACHE_SHOP_KEY + id; //1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.是否存在 if (!StringUtil.isNullOrEmpty(shopJson)) { //3.存在 返回商铺信息 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } //判断命中的是否是空值 if (shopJson != null) { return null; } //4.实现缓存重建 String lockKey = LOCK_SHOP_KEY + id; Shop shop = null; try { //4.1获取互斥锁 boolean isLock = tryLock(lockKey); //4.2判断是否成功 if (!isLock) { //4.3失败,则休眠并重试 Thread.sleep(50); return queryWithMutex(id); } //4.4 再次检查缓存是否存在 String shopJson2 = stringRedisTemplate.opsForValue().get(key); if (!StringUtil.isNullOrEmpty(shopJson2)) { //4.5 存在 返回商铺信息 shop = JSONUtil.toBean(shopJson2, Shop.class); return shop; } //查缓存不存在 //4.6成功根据id在数据库中查询商铺信息 shop = getById(id); //模拟重建延时 Thread.sleep(200); //5.不存在 返回错误信息 if (shop == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //6.存在 将商铺信息写入redis中 stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (Exception e) { throw new RuntimeException(e); } finally { //7.释放互斥锁 unLock(lockKey); } //8.返回 return shop; } /** * 获取锁 * * @param key * @return */ private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } /** * 释放锁 * * @param key * @return */ private void unLock(String key) { stringRedisTemplate.delete(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/** * 创建线程池 */ private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); /** * 逻辑过期 * * @param id * @return */ public Shop queryWithLogicalExpire(Long id) { String key = CACHE_SHOP_KEY + id; //1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.是否存在 if (StringUtil.isNullOrEmpty(shopJson)) { //3.不存在 直接返回 return null; } //4.命中,需要先把json反序列化成对象 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.缓存重建 String lockKye = LOCK_SHOP_KEY + id; //6.1获取互斥锁 //6.2判断是否获取锁成功 boolean isLock = tryLock(lockKye); if (isLock) { //6.3成功开启,独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { // 重建缓存 this.saveShop2Redis(id, 20L); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁 unLock(lockKye); } }); } //6.4返回过期店铺信息 return shop; }
封装缓存工具类
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为ison并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165package com.hmdp.utils; import cn.hutool.core.util.BooleanUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.hmdp.entity.Shop; import io.netty.util.internal.StringUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Function; import static com.hmdp.utils.RedisConstants.*; @Slf4j @Component public class CacheClient { private final StringRedisTemplate stringRedisTemplate; public CacheClient(StringRedisTemplate stringRedisTemplate){ this.stringRedisTemplate = stringRedisTemplate; } /** * 方法1:将任意Java对象序列化为ison并存储在string类型的key中,并且可以设置TTL过期时间 * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题 * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题 * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题 */ public void set(String key , Object value, Long time, TimeUnit unit){ stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit); } public void setWithLogicalExpire(String key , Object value, Long time, TimeUnit unit){ //设置逻辑过期 RedisData redisData = new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); //写入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } /** * 缓存穿透 * * @param id * @return */ 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 (!StringUtil.isNullOrEmpty(json)) { //3.存在 返回商铺信息 R r = JSONUtil.toBean(json,type); return r; } //判断命中的是否是空值 if (json != null) { return null; } //4.不存在根据id在数据库中查询商铺信息 R r = dbFallback.apply(id); //5.不存在 返回错误信息 if (r == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //6.存在 将商铺信息写入redis中 调用方法1 this.set(key,r,time,unit); return r; } /** * 创建线程池 */ private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); /** * 逻辑过期 * * @param id * @return */ public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit) { String key = CACHE_SHOP_KEY + id; //1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.是否存在 if (StringUtil.isNullOrEmpty(shopJson)) { //3.不存在 直接返回 return null; } //4.命中,需要先把json反序列化成对象 RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); JSONObject data = (JSONObject) redisData.getData(); R r = JSONUtil.toBean(data, type); LocalDateTime expireTime = redisData.getExpireTime(); //5.判断是否过期 if (expireTime.isAfter(LocalDateTime.now())) { //5.1未过期,直接返回店铺信息 return r; } //5.2已过期 //6.缓存重建 String lockKye = LOCK_SHOP_KEY + id; //6.1获取互斥锁 //6.2判断是否获取锁成功 boolean isLock = tryLock(lockKye); if (isLock) { //6.3成功开启,独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { //查询数据库 R r1 = dbFallback.apply(id); //写入redis setWithLogicalExpire(key,r1,time,unit); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁 unLock(lockKye); } }); } //6.4返回过期店铺信息 return r; } /** * 获取锁 * * @param key * @return */ private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } /** * 释放锁 * * @param key * @return */ private void unLock(String key) { stringRedisTemplate.delete(key); } }
项目源码
https://toscode.gitee.com/wanghuaiyu666/hm-dianping
欢迎大家到我的仓库下载项目源码
最后
以上就是清脆鸵鸟最近收集整理的关于Redis实战篇--商户查询缓存Redis实战篇–商户查询缓存的全部内容,更多相关Redis实战篇--商户查询缓存Redis实战篇–商户查询缓存内容请搜索靠谱客的其他文章。
发表评论 取消回复