缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。


基于互斥锁的解决方式
原理:Redis有个命令:sexnx
centos7-192.168.163.129:0>setnx lock 1
"1"
centos7-192.168.163.129:0>setnx lock 2
"0"
centos7-192.168.163.129:0>setnx lock 3
"0"
centos7-192.168.163.129:0>get lock
"1"
centos7-192.168.163.129:0>del lock
"1"
centos7-192.168.163.129:0>get lock
null
centos7-192.168.163.129:0>setnx lock 2
"1"
setnx的时候,当key=lock,存在时,不能更新,只有不存在的时候才能set

1.0版本
/**
* 互斥锁解决缓存击穿(热点key)
* @param id
* @return
*/
public Shop queryWithMutex(Long id){
Shop shop =null;
String key =CACHE_SHOP_KEY + id;
String JSONStr = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(JSONStr)){
return JSONUtil.toBean(JSONStr, Shop.class);
}
if ("".equals(JSONStr)){
return null;
}
try {
boolean isLock = tryLock(LOCK_SHOP_KEY + id);
if (!isLock) {
Thread.sleep(50);
return queryWithMutex(id);
}
shop = getById(id);
Thread.sleep(200);
if (shop ==null){
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
unLock(key);
}
return shop;
}
/**
* 加互斥锁
* @param key
* @return
*/
public boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
* @return
*/
public void unLock(String key){
stringRedisTemplate.delete(key);
}
2.0版本,封装
/**
* 缓存击穿-->互斥锁解决
* @param prefix 查询的前缀
* @param id
* @param type
* @param function
* @param time
* @param unit
* @return
* @param <R>
* @param <ID>
*/
public <R,ID> R queryWithMutex(String prefix, ID id, Class<R> type, Function<ID,R> function, Long time, TimeUnit unit ){
String key = prefix + id;
String lockKey = LOCK_SHOP_KEY+id;
String JSON = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(JSON)){
return JSONUtil.toBean(JSON,type);
}
if ("".equals(JSON)){
return null;
}
R r = null;
try {
boolean isLock = tryLock(lockKey);
if (!isLock){
Thread.sleep(50);
return queryWithMutex(prefix, id, type, function, time, unit);
}
r = function.apply(id);
log.debug("r--->" + r);
// 代表数据库没有需要的数据
if (r==null){
set(key,"", time, unit);
return null;
}
set(key,JSONUtil.toJsonStr(r),time,unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unLock(lockKey);
}
return r;
}
/**
* 互斥锁
* @param key
* @return
*/
public boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
public void unLock(String key){
stringRedisTemplate.delete(key);
}
基于逻辑过期的解决方式

1.0版本
/**
* 逻辑过期处理 预热
* @param id
*/
public void saveShopHot(Long id,Long seconds){
RedisData redisData = new RedisData();
redisData.setData(getById(id));
redisData.setExpireTime(LocalDateTime.now().plusSeconds(seconds));
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
// 创建线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 逻辑过期解决缓存击穿
* @param id
* @return
*/
public Shop queryWithExpiration(Long id){
String JSONStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 未命中
if (StringUtils.isBlank(JSONStr)){
return null;
}
// 命中
LocalDateTime now = LocalDateTime.now();
RedisData redisData = JSONUtil.toBean(JSONStr, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
// 没过期,返回商铺信息
if (expireTime.isAfter(now)){
return shop;
}
// 过期
if (tryLock(LOCK_SHOP_KEY + id)){
// 获取到了锁,开启一个新线程,查询数据库,写入redies,重新设置过期时间
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
saveShopHot(id,30L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
unLock(LOCK_SHOP_KEY + id);
}
});
}
return shop;
}
2.0版本,封装
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 缓存击穿,逻辑过期解决
* @param prefix
* @param id
* @param type
* @param function
* @param time
* @param unit
* @return
* @param <R>
* @param <ID>
*/
public <R,ID> R queryWithLogicalExpire(String prefix, ID id, Class<R> type, Function<ID,R> function, Long time, TimeUnit unit){
String key = prefix + id;
String lockKey = LOCK_SHOP_KEY + id;
String JSON = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(JSON)){
return null;
}
RedisData redisData = JSONUtil.toBean(JSON, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
LocalDateTime now = LocalDateTime.now();
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
if (expireTime.isAfter(now)){
// 没过期
return r;
}
// 过期
try {
if (tryLock(lockKey)) {
CACHE_REBUILD_EXECUTOR.submit(()->{
log.debug("竞争到了");
R rDb = function.apply(id);
setWithLogicalExpire(key,rDb,time,unit);
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(key);
}
return r;
}
/**
* 重载缓存
* @param key
* @param value
* @param time
* @param unit
*/
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
/**
* 设置逻辑过期缓存
* @param key
* @param value
* @param time
* @param 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)));
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}