目录
引言
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮
解决办法
互斥锁(强一致,性能差)
根据图片就可以看出,我们的思路就是只能让一个线程能够进行访问Redis,要想实现这个功能,我们也可以使用Redis自带的setnx
封装两个方法,一个写key来尝试获取锁另一个删key来释放锁
/**
* 尝试获取锁
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
在并行情况下每当其他线程想要获取锁,来访问缓存都要通过将自己的key写到tryLock()方法里,setIfAbsent()返回false则说明有线程在在更新缓存数据,锁未释放。若返回true则说明当前线程拿到锁了可以访问缓存甚至操作缓存。
我们在下面一个热门的查询场景中用代码用代码来实现互斥锁解决缓存击穿,代码如下:
/**
* 解决缓存击穿的互斥锁
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
//1.从Redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key); //JSON格式
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) { //不为空就返回 此工具类API会判断" "为false
//存在则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//return Result.ok(shop);
return shop;
}
//3.判断是否为空值 这里过滤 " "的情况,不用担心会一直触发这个条件因为他有TTL
if (shopJson != null) {
//返回一个空值
return null;
}
//4.缓存重建 Redis中值为null的情况
//4.1获得互斥锁
String lockKey = "lock:shop"+id;
Shop shopById=null;
try {
boolean isLock = tryLock(lockKey);
//4.2判断是否获取成功
if (!isLock){
//4.3失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//4.4成功,根据id查询数据库
shopById = getById(id);
//5.不存在则返回错误
if (shopById == null) {
//将空值写入Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//为什么这里要存一个" "这是因为如果后续DB中有数据补充的话还可以去重建缓存
//return Result.fail("暂无该商铺信息");
return null;
}
//6.存在,写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.释放互斥锁
unlock(lockKey);
}
return shopById;
}
逻辑过期(高可用,性能优)
方案:用户查询某个热门产品信息,如果缓存未命中(即信息为空),则直接返回空,不去查询数据库。如果缓存信息命中,则判断是否逻辑过期,未过期返回缓存信息,过期则重建缓存,尝试获得互斥锁,获取失败则直接返回已过期缓存数据,获取成功则开启独立线程去重构缓存然后直接返回旧的缓存信息,重构完成之后就释放互斥锁。
封装一个方法用来模拟更新逻辑过期时间与缓存的数据在测试类里运行起来达到数据与热的效果
/**
* 添加逻辑过期时间
*
* @param id
* @param expireTime
*/
public void saveShopRedis(Long id, Long expireTime) {
//查询店铺信息
Shop shop = getById(id);
//封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
//将封装过期时间和商铺数据的对象写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
查询接口:
/**
* 逻辑过期解决缓存击穿
*
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
String key = CACHE_SHOP_KEY + id;
Thread.sleep(200);
//1.从Redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key); //JSON格式
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//不存在则直接返回
return null;
}
//3.判断是否为空值
if (shopJson != null) {
//返回一个空值
//return Result.fail("店铺不存在!");
return null;
}
//4.命中
//4.1将JSON反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//4.2判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.未过期则返回店铺信息
return shop;
}
//6.过期则缓存重建
//6.1获取互斥锁
String LockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(LockKey);
//6.2判断是否成功获得锁
if (isLock) {
//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;
}
设计逻辑过期时间
可以用这个方法设置逻辑过期时间
import org.redisson.Redisson;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonExample {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
String key = "exampleKey";
String value = "exampleValue";
int timeout = 10; // 过期时间(秒)
// 获取RBucket对象
RBucket<String> bucket = redisson.getBucket(key);
// 设置值并指定过期时间
bucket.set(value, timeout, TimeUnit.SECONDS);
System.out.println("设置成功");
redisson.shutdown();
}
}
大家可以看到,逻辑过期锁就是可以实现并发,所以他的效率更快,性能更好
但是
牺牲了数据的实时性,以保证高并发场景下的服务可用性和数据库的稳定性。
在实际应用中,需要确保获取互斥锁的操作是原子的,并且锁具有合适的超时时间,以避免死锁的发生。
逻辑过期策略适用于那些对数据实时性要求不高,但要求服务高可用性的场景。