Redis缓存击穿
Redis缓存击穿也叫热点key问题,就是在一个被高并发访问并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,缓存的重建也比较耗时, 存在多个请求同时在重建缓存
常见的解决方案有两种
- 互斥锁
优点:没有额外的内存消耗、保证一致性、实现起来简单
缺点:线程需要等待,影响性能、可能存在死锁风险 - 逻辑过期(不设置TTL人为的设置一个过期时间字段,代码逻辑查询是否需要更新数据)
优点:线程无需等待,性能较好
缺点:不保证一致性、有额外的内存消耗、实现复杂
互斥锁相关示例代码
互斥锁才用redis的setnx特性,存在的数据插入将不覆盖原有数据,并返回操作数量,若存在该key的数据,则返回 0
获取锁
/**
* 尝试获取锁
* @param key
* @return
*/
private boolean tryLock(String key){
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(aBoolean);
}
释放锁
/**
* 释放锁
* @param key
*/
private void unLock(String key){
redisTemplate.delete(key);
}
整体使用示例
public Shop queryWithMutex(Long id) throws InterruptedException {
// 1.从redis中查询商铺缓存
String shopJson = redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在 直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
// 判断命中的是否为空值
if (shopJson != null){
return null;
}
// 尝试获取锁
boolean isLock = tryLock(RedisConstants.LOCK_SHOP_KEY + id);
// 若没获取到锁,休眠一段时间后重新查询redis
if (!isLock){
Thread.sleep(1000);
queryWithMutex(id);
}
// 若获取到锁,重建缓存
Shop shop = null;
try {
// 4.不存在 查询mysql
shop = getById(id);
if (shop == null){
// 5.不存在 返回错误
// 给缓存设置空值,避免存在缓存穿透的问题
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6.存在 写会redis
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.LOCK_SHOP_TTL, TimeUnit.MINUTES);
}
catch (Exception exception){
throw new RuntimeException(exception);
}
finally {
unLock(RedisConstants.LOCK_SHOP_KEY + id);
}
return shop;
}
逻辑过期示例代码
定义一个RedisData实体 来存放实体数据及过期时间 如下:
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
保存数据相关代码 不设置ttl 手动设置一个过期时间字段 来判断时间是否过期
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
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
逻辑过期 查询及缓存更新相关示例代码
/**
* 逻辑过期解决方案
*/
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
// 1.从redis中查询商铺缓存
String shopJson = redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
return null;
}
// 存在 获取数据,判断是否存在逻辑过期
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
// 判断是否存在逻辑过期
boolean isExpire = redisData.getExpireTime().isAfter(LocalDateTime.now());
// 如果已经过去 开启一个新的线程 重新获取数据 并重建缓存
if (!isExpire){
// 尝试获取锁
boolean isLock = tryLock(RedisConstants.LOCK_SHOP_KEY + id);
// 判断锁是否获取成功
if (!isLock){
// 未获取到锁 返回过期的商户信息
return shop;
}
// 成功 查询数据 重新建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
// 重建缓存
try {
Thread.sleep(200);
saveShop2Redis(id, 4L);
} catch (Exception e){
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(RedisConstants.LOCK_SHOP_KEY + id);
}
});
}
return shop;
}
评论区