什么是缓存击穿
- 缓存击穿问题也叫热点Key问题,
- 就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,
- 无数的请求访问会在瞬间给数据库带来巨大的冲击。
店铺查询 + 互斥锁的逻辑图
public Shop querywithjichuan_mutex(Long id) 逻辑
1. 先根据key查询Redis
- 以
"cache:shop:"+id为key,从Redis中提取String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
2. 若有数据,直接返回shop
- 若shopJson非null也非空字符串,则说明Redis中有数据,直接转换为shop实体返回。
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
3. 查到空字符串,则返回失败
- 若为空字符串,则(数据库中也没),直接返回null,失败。
if (shopJson != null) {
return null;
}
4. 获取互斥锁,再去查询数据库
4.0 锁的实现
- 加锁:在Redis中设置key,value为字符串1,过期时间为10s(尝试设置键 "myLock",如果该键不存在,则设置成功并在 10 秒后过期;如果已存在,则设置失败。)
- 释放锁,把Redis中该key删了。
/*
下面用来解决热点高并发访问中的缓存击穿问题
实现了一个基于 Redis 的分布式锁机制,包含获取锁和释放锁的逻辑。
*/
/*
1. 获取锁的逻辑:
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 避免返回值为null,我们这里使用了BooleanUtil工具类
return BooleanUtil.isTrue(flag);
}
// 2. 释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
4.1 尝试获取锁
- 调用基于Redis的加锁操作,
- 若获取失败,则sleep 50ms再尝试
while (!flag) {
Thread.sleep(50);
return querywithjichuan_mutex(id);
}
5.1 数据库也没有,就Redis中存空字符串
- 根据id获取shop实体
- 若shop为null,即数据库也无,则在缓存中存空字符串
- key为
"cache:shop:"+id
- value为空字符串
- 过期时间为2min
if (shop == null) {
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
5.2 数据库中有,就重建Redis
- shop转为Json字符串存入Redis
- key为
"cache:shop:"+id
- value就是shop的json字符串,也要有个过期时间,30min
String jsonStr = JSONUtil.toJsonStr(shop);
// 并存入redis,设置TTL
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
6 释放锁
unlock(LOCK_SHOP_KEY + id);
示例代码
/*
二、缓存击穿的解决:法一,互斥锁
引入“互斥锁”来确保只有一个线程去查询数据库和重建缓存,其他线程等待或重试。
*/
public Shop querywithjichuan_mutex(Long id) {
// 1. 先从Redis中查,这里的常量值是固定的前缀 + 店铺id
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
/*
* 2. 判断缓存是否命中
* 2.1 缓存命中了
* 2.1.1 如果 shopJson 不为空(字符串不是 null、""、全空格),说明缓存中有数据。
* 把 json 字符串转为 Shop 对象直接返回,不需要访问数据库。
*/
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
/*
* 2.1.2 如果查询到的是空字符串“”,则说明是我们缓存的空数据
* 之前数据库查不到这条数据,我们会把一个空字符串放进缓存。
* 这样后续请求查到这个空字符串就不会再去访问数据库,避免缓存穿透。
*/
if (shopJson != null) {
return null;
}
/*
* 2.2 缓存未命中,开始加锁重建缓存
* 实现在高并发的情况下缓存重建
* 调用 tryLock() 尝试获取互斥锁(通常是 Redis 的 SETNX 实现)。
*/
Shop shop = null;
try {
// 获取互斥锁
boolean flag = tryLock(LOCK_SHOP_KEY + id);
/*
* 2.2.1 加锁失败就休眠重试(自旋)
* 如果拿不到锁,说明其他线程正在重建缓存。
* 当前线程休眠 50 毫秒后递归调用自己重新尝试查询缓存。
* 这个过程称为“自旋锁”或“延迟重试”。
*/
while (!flag) {
Thread.sleep(50);
return querywithjichuan_mutex(id);
}
// 2.2.2 获取成功->读取数据库,重建缓存
// 2.2.2.1 查不到,则将空值写入Redis
shop = getById(id);
if (shop == null) {
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
/*
* 2.2.2.2 数据库中有数据,写入缓存
* 查到了则转为json字符串
*/
String jsonStr = JSONUtil.toJsonStr(shop);
// 并存入redis,设置TTL
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 最终把查询到的商户信息返回给前端
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 2.2.3 最后:释放锁
unlock(LOCK_SHOP_KEY + id);
}
return shop;
}