黑马点评02:商户查询缓存

商户查询缓存流程

img

缓存更新策略

先更新数据库,再删除缓存

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


@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {


@Autowired
private StringRedisTemplate redisTemplate;

// 查询单个店铺(带缓存)
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;

// 1. 从缓存查询
String shopJson = redisTemplate.opsForValue().get(key);

// 2. 缓存命中
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}

// 3. 缓存未命中,查数据库
Shop shop = getById(id);
if (shop == null) {
return Result.fail("店铺不存在");
}

// 4. 写入缓存(设置过期时间)
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
CACHE_SHOP_TTL, TimeUnit.MINUTES);

return Result.ok(shop);
}

// 更新店铺(先更新数据库,再删除缓存)
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺ID不能为空");
}

// 1. 更新数据库
updateById(shop);

// 2. 删除缓存
String key = CACHE_SHOP_KEY + id;
redisTemplate.delete(key);

return Result.ok();
}

}

先更新数据库再删除缓存,即使有短暂不一致,也不会产生脏数据;而先删缓存再更新数据库,在并发下会产生难以消除的脏数据。

缓存穿透

核心思路:查询不到数据时,缓存一个 null 或特殊标记,设置较短的过期时间。

img

redis:"" (空字符串),则结束;null (不存在),则查数据库

缓存雪崩

设置随机过期时间

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


@Sevice
public class ShopService {


private static final long BASE_TTL = 3600; // 基础1小时
private static final Random RANDOM = new Random();

public void saveShopToCache(Shop shop) {
String key = "shop:" + shop.getId();

// 1小时 + 随机0~300秒,避免同时过期
long randomOffset = RANDOM.nextInt(300); // 0-300秒
long ttl = BASE_TTL + randomOffset;

stringRedisTemplate.opsForValue().set(
key,
JSONUtil.toJsonStr(shop),
ttl,
TimeUnit.SECONDS
);
}

}

缓存击穿

逻辑过期(永不过期 + 异步更新)

image-20260923214417921

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


@Data
public class RedisData<T> {
private T data;
private LocalDateTime expireTime; // 逻辑过期时间
}

@Service
@Slf4j
public class ShopService {


@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private ExecutorService executorService; // 线程池

public Shop getShopById(Long id) {
String key = "shop:" + id;

// 1. 查缓存
String json = redisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json)) {
// 缓存不存在(首次加载),查数据库
return loadShopFromDB(id);
}

// 2. 反序列化
RedisData<Shop> redisData = JSONUtil.toBean(json, RedisData.class);
Shop shop = redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();

// 3. 判断是否逻辑过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return shop;
}

// 4. 已过期,尝试获取锁去异步更新
String lockKey = "lock:shop:" + id;
RLock lock = redissonClient.getLock(lockKey);

// 尝试获取锁(非阻塞)
boolean isLock = lock.tryLock();
if (isLock) {
try {
// 异步更新缓存(不阻塞当前请求)
executorService.submit(() -> {
try {
Shop newShop = shopMapper.selectById(id);
RedisData<Shop> newRedisData = new RedisData<>();
newRedisData.setData(newShop);
newRedisData.setExpireTime(LocalDateTime.now().plusMinutes(30));
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newRedisData));
} finally {
lock.unlock();
}
});
} catch (Exception e) {
log.error("异步更新失败", e);
}
}

// 5. 返回旧数据(即使过期了)
return shop;
}

private Shop loadShopFromDB(Long id) {
Shop shop = shopMapper.selectById(id);
if (shop != null) {
// 写入缓存,逻辑过期时间 30 分钟
RedisData<Shop> redisData = new RedisData<>();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusMinutes(30));
redisTemplate.opsForValue().set("shop:" + id, JSONUtil.toJsonStr(redisData));
}
return shop;
}

}