2025.08.18

黑马点评开发日志Day02——商户查询缓存

一、缓存

1.1 介绍

  • 缓存就是数据交换的缓冲区(称作Cache),是存储数据的临时地方,一般读写性能较高
    缓存
  • 缓存的作用:
    • 降低后端负载
    • 提高读写效率,降低响应时间
  • 缓存的成本:
    • 数据一致性成本
    • 代码维护成本
    • 运维成本

二、商户查询缓存

2.1 商户查询缓存——需求分析

  • 业务流程:
    商户查询缓存

2.2 商户查询缓存——代码实现

2.2.1 ShopController

1
2
3
4
5
6
7
8
9
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}

2.2.2 IShopService

1
2
3
4
5
6
/**
* 根据id查询商铺信息
* @param id
* @return
*/
Result queryById(Long id);

2.2.3 ShopServiceImpl

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
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

@Resource
private StringRedisTemplate stringRedisTemplate;

/**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 1. 从redis中查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(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));
// 7. 返回
return Result.ok(shop);
}
}

2.3 商户查询缓存——功能测试

  • 商铺查询缓存
    商铺查询缓存
    Redis缓存

三、商铺类型查询缓存

3.1 商铺类型查询缓存——需求分析

  • 业务需求:
    商铺类型查询缓存

3.2 商铺类型查询缓存——代码实现

3.2.1 ShopTypeController

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;

@GetMapping("list")
public Result queryTypeList() {
// List<ShopType> types = typeService.query().orderByAsc("sort").list();
List<ShopType> typeList = typeService.queryTypeList();
return Result.ok(typeList);
}
}

3.2.2 IShopTypeService

1
2
3
4
5
public interface IShopTypeService extends IService<ShopType> {


List<ShopType> queryTypeList();
}

3.2.3 ShopTypeServiceImpl

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
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public List<ShopType> queryTypeList() {
// 1. 从redis中查询商铺类型缓存
String shopTypeJson = stringRedisTemplate.opsForValue().get(LOGIN_SHOP_TYPE_KEY);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopTypeJson)) {
// 3. 存在,转化为List后返回
List<ShopType> shopTypeList = JSONUtil.toList(shopTypeJson, ShopType.class);
return shopTypeList;
}
// 4. 不存在,去数据库里找
List<ShopType> shopTypeList = this.query().orderByAsc("sort").list();
// 5. 判断数据库里有没有
if (shopTypeList.isEmpty()) {
// 6. 返回空列表
return new ArrayList<>();
}
// 7. 有的话写入redis
stringRedisTemplate.opsForValue().set(LOGIN_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shopTypeList));
// 8. 返回
return shopTypeList;

}
}

3.3 商铺类型查询缓存——功能测试

  • 商铺类型查询缓存
    商铺类型查询缓存
    Redis缓存

四、缓存更新策略

4.1 缓存更新策略分析

  • 缓存更新策略
内存淘汰 超时剔除 主动更新
说明 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存 给缓存数据添加TTL时间,到期后自动删除缓存,下次查询时更新缓存 编写业务逻辑,在修改数据库的同时,更新缓存
一致性 一般
维护成本
  • 业务场景:
    • 低一致性需求:使用内存淘汰机制,例如店铺类型的查询缓存
    • 高一致性需求:使用主动更新策略,并以超时剔除作为兜底方案,例如店铺详情查询的缓存

4.2 主动更新策略

  • Cache Aside Pattern(缓存旁路模式):由缓存的调用者,在更新数据库的同时更新缓存

  • Read/Write Through Pattern(读写穿透模式):缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题

  • Write-Behind Cache Pattern(写后模式):调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致

  • Cache Aside Pattern需要考虑的问题:

    • 删除缓存还是更新缓存
      • 更新缓存:每次更新数据库都更新缓存,无效写操作较多(×)
      • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(√)
    • 如何保证缓存与数据库的操作的同时成功或失败?
      • 单体系统,将缓存与数据库操作放在一个事务
      • 分布式系统,利用TCC等分布式事务方案
    • 先操作缓存还是先操作数据库
      • 先删除缓存,再操作数据库
      • 先操作数据库,再删除缓存


  • 缓存更新策略的最佳实践方案:

    • 低一致性需求:使用Redis自带的内存淘汰机制
    • 高一致性需求:主动更新,并以超时剔除作为兜底方案
      • 读操作:
        • 缓存命中则直接返回
        • 缓存未命中则查询数据库,并写入缓存,设定超时时间
      • 写操作:
        • 先写数据库,然后再删除缓存
        • 要确保数据库与缓存操作的原子性

4.3 给查询商铺的缓存添加超时剔除和主动更新的策略——需求分析

  • 需求分析:修改ShopController中的业务逻辑,满足下面的需求:
    • 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
    • 根据id修改店铺时,先修改数据库,再删除缓存

4.4 给查询商铺的缓存添加超时剔除和主动更新的策略——代码实现

4.4.1 ShopController

1
2
3
4
5
6
7
8
9
10
/**
* 更新商铺信息
* @param shop 商铺数据
* @return
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}

4.4.2 IShopService

1
2
3
4
5
6
/**
* 更新商铺信息
* @param shop
* @return
*/
Result update(Shop shop);

4.4.3 ShopServiceImpl

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
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

@Resource
private StringRedisTemplate stringRedisTemplate;

/**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 1. 从redis中查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(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), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7. 返回
return Result.ok(shop);
}

/**
* 更新商铺信息
* @param shop
* @return
*/
@Override
@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);
// 3. 返回
return Result.ok();
}
}

4.5 给查询商铺的缓存添加超时剔除和主动更新的策略——功能测试

  • 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  • 根据id修改店铺时,先修改数据库,再删除缓存

五、缓存相关问题

5.1 缓存穿透

  • 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库
  • 常见的解决方案有两种:
    • 缓存空对象
      • 优点:实现简单,维护方便
      • 缺点:
        • 额外的内存消耗
        • 可能造成短期的不一致
    • 布隆过滤
      • 优点:内存占用较少,没有多余key
      • 缺点:
        • 实现复杂
        • 存在误判可能

缓存穿透

5.2 解决商铺查询中的缓存穿透

5.2.1 需求分析

商铺查询中的缓存穿透

5.2.2 代码实现

  • ShopServiceImpl
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
/**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 1. 从redis中查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回错误信息
return Result.fail("店铺信息不存在!");
}

// 4. 不存在,根据id查询数据库
Shop shop = getById(id);
// 5. 不存在,返回错误
if (shop == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return Result.fail("店铺信息不存在!");
}
// 6. 存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7. 返回
return Result.ok(shop);
}

5.2.3 功能测试

  • 解决商铺查询中的缓存穿透
  • 第一次查询时有select操作,第二次查询时没有select操作

5.3 缓存雪崩

  • 缓存雪崩是指缓存中大量数据在同一时间过期,导致大量请求直接打到数据库上,造成数据库压力过大甚至宕机
    缓存雪崩
  • 解决方案:
    • 给不同的Key的TTL添加随机值
    • 利用Redis集群提高服务的可用性
    • 给缓存业务添加降级限流策略
    • 给业务添加多级缓存

5.4 缓存击穿

  • 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
  • 常见的解决方案有两种:
    • 互斥锁
    • 逻辑过期

缓存击穿

互斥锁与逻辑过期

  • 互斥锁
    • 优点:
      • 没有额外的内存消耗
      • 保证一致性
      • 实现简单
    • 缺点:
      • 线程需要等待,性能受影响
      • 可能有死锁风险
  • 逻辑过期
    • 优点:
      • 线程无需等待,性能较好
    • 缺点:
      • 不保证一致性
      • 有额外内存消耗
      • 实现复杂

5.5 基于互斥锁解决缓存击穿问题

5.5.1 需求分析

  • 需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
    基于互斥锁解决缓存击穿问题

5.5.2 代码实现

  • ShopServiceImpl
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
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

@Resource
private StringRedisTemplate stringRedisTemplate;

/**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queryWithPassThrough(id);

// 互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail("店铺信息不存在!");
}
// 7. 返回
return Result.ok(shop);
}

// 互斥锁解决缓存击穿
public Shop queryWithMutex(Long id) {
// 1. 从redis中查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3. 存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回错误信息
return null;
}

// 4. 开始实现缓存重建
// 4.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 判断是否获取成功
if (!isLock) {
// 4.3 失败则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 4.4 成功则根据id查询数据库
shop = getById(id);
// 5. 不存在,返回错误
if (shop == null) {
// 将空值写入redis
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 (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7. 释放互斥锁
unlock(lockKey);
}
// 8. 返回
return shop;
}

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

// 释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}

5.6 基于逻辑过期解决缓存击穿问题

5.6.1 需求分析

  • 需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
    基于逻辑过期解决缓存击穿问题

5.6.2 代码实现

  • ShopServiceImpl
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
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

@Resource
private StringRedisTemplate stringRedisTemplate;

/**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = queryWithPassThrough(id);

// 互斥锁解决缓存击穿
// Shop shop = queryWithMutex(id);

// 逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
if (shop == null) {
return Result.fail("店铺信息不存在!");
}
// 7. 返回
return Result.ok(shop);
}

// 线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

// 逻辑过期解决缓存击穿
public Shop queryWithLogicalExpire(Long id) {
// 1. 从redis中查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isBlank(shopJson)) {
// 3. 未命中,直接返回
return null;
}
// 4. 命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1 未过期,直接返回店铺信息
return shop;
}
// 5.2 已过期,需要缓存重建
// 6. 缓存重建
// 6.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2 判断是否获取成功
if (isLock) {
// 获取成功后应再次检测redis缓存是否过期,做DoubleCheck,如果未过期则无需重建缓存
// 6.3 获取成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4 返回过期的商铺信息
return shop;
}

public void saveShop2Redis(Long id, Long expireSeconds) {
// 1. 查询店铺数据
Shop shop = getById(id);
// 2. 封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3.写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

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

// 释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}

5.7 缓存工具封装

  • 基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
    • 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
    • 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
    • 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
    • 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

5.7.1 CacheClient

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
@Slf4j
@Component
public class CacheClient {

private final StringRedisTemplate stringRedisTemplate;

public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}

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));
}

// 缓存穿透
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 从redis中查询商铺缓存
String key = keyPrefix + id;
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);
// 7. 返回
return r;
}

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

// 逻辑过期解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 从redis中查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isBlank(shopJson)) {
// 3. 未命中,直接返回
return null;
}
// 4. 命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1 未过期,直接返回店铺信息
return r;
}
// 5.2 已过期,需要缓存重建
// 6. 缓存重建
// 6.1 获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2 判断是否获取成功
if (isLock) {
// 获取成功后应再次检测redis缓存是否过期,做DoubleCheck,如果未过期则无需重建缓存
// 6.3 获取成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
// 先查数据库
R r1 = dbFallback.apply(id);
// 写入redis
this.setWithLogicalExpire(key, r1, time, unit);

} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4 返回过期的商铺信息
return r;
}

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

// 释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}

5.7.2 ShopServiceImpl

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
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private CacheClient cacheClient;

/**
* 根据id查询商铺信息
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 缓存穿透
// Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

// 逻辑过期解决缓存击穿
Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
if (shop == null) {
return Result.fail("店铺信息不存在!");
}
// 7. 返回
return Result.ok(shop);
}

/**
* 更新商铺信息
* @param shop
* @return
*/
@Override
@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);
// 3. 返回
return Result.ok();
}
}