- 👋 我是反钉党,大三本科生
- 👀 我的兴趣是游戏、计算机、旅游、饥荒
- 🌱 我正在学习计算机
- 💞️ 一起学习进步
- 📫 邮箱:3217998214@qq.com
- 😄 个人网站 http://47.96.86.223/
分享一张壁纸:
缓存击穿
缓存击穿也叫热点key问题,就是一个被高并发访问且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案
方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗 保证一致性 实现简单 | 线程需要等待 性能影响 |
逻辑过期 | 线程无需等待 性能好 | 不保证一致性 有额外内存消耗 实现复杂 |
互斥锁实现
使用 redis 的 setnx
指令实现:
setnx lock 1
如果存在,输出1,不存在输出0。
获取锁:
private boolean tryLock(String key) {
// 有效期一般是比业务的执行时间长一点即可
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 3, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
释放锁:
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
private Shop queryWithMutex(Long id) {
// 1. 从缓存查询
String key = RedisConstants.CACHE_SHOP_KEY + id;
String jsonStr = stringRedisTemplate.opsForValue().get(key);
if (jsonStr != null) {
if (jsonStr.isEmpty()) {
return null;
} else {
return JSONUtil.toBean(jsonStr, Shop.class);
}
}
// 2. 缓存中不存在,重建缓存
Shop shop;
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
try {
if (!isLock) {
// 没有拿到锁,休眠一会,重新查询
Thread.sleep(50);
// 重新在缓存中查询数据
return queryWithMutex(id);
} else {
// 缓存重建
shop = getById(id);
if (shop == null) {
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
} else {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
return shop;
}
逻辑过期
定义一个新的Redis存储对象,加上逻辑过期时间字段:
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
这个方案需要进行缓存预热,要提前把数据加入到缓存里面,上代码:
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 逻辑过期解决缓存穿透
public Shop queryWithLogicalExpire(Long id) {
// 1. 从缓存查询
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 2. 查询是否存在
String jsonStr = stringRedisTemplate.opsForValue().get(key);
// 3. 命中,需要判断过期时间
LocalDateTime now = LocalDateTime.now();
RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
// 4. 如果过期,获取互斥锁开启新线程执行缓存重建
if (redisData.getExpireTime().isBefore(now)) {
// 5. 执行缓存重建
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
// 开启新线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 缓存重建
rebuildCache(id, now);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
});
}
}
// 6. 不管有没有过期,都返回旧的数据
return JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
}
public void rebuildCache(Long id, LocalDateTime now) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
Shop shop = getById(id);
if (shop == null) {
stringRedisTemplate.opsForValue().set(key,"", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
} else {
RedisData redisData = new RedisData();
redisData.setExpireTime(now.plusSeconds(RedisConstants.CACHE_SHOP_TTL));
redisData.setData(shop);
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
}
缓存工具类封装
主要解决缓存穿透、缓存击穿问题,使用双重检查锁定,锁的实现可以改成分布式锁,这里是简单的实现:
@Component
public class RedisUtil {
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
private final StringRedisTemplate stringRedisTemplate;
@Value("${redis-util.key-prefix}")
private String keyPrefix;
@Value("${redis-util.null-value}")
private String nullValue;
@Value("${redis-util.null-value-expire}")
private Long nullValueExpire;
public RedisUtil(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public <T> void set(String key, T value) {
key = keyPrefix + key;
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
}
public <T> void set(String key, T value, Long expireTime, TimeUnit timeUnit) {
key = keyPrefix + key;
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
}
public <T> void setWithLogicalExpireTime(String key, T value, Long expireTime, TimeUnit timeUnit) {
LocalDateTime now = LocalDateTime.now();
key = keyPrefix + key;
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(now.plusSeconds(timeUnit.toSeconds(expireTime)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback) {
key = keyPrefix + key;
String json = stringRedisTemplate.opsForValue().get(key);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
return JSONUtil.toBean(json, type);
}
}
boolean isLock = tryLock(key);
if (isLock) {
try {
json = stringRedisTemplate.opsForValue().get(key);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
return JSONUtil.toBean(json, type);
}
}
T value = dbCallback.execute();
if (value == null) {
set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
} else {
set(key, value);
}
return value;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(key);
}
} else {
try {
Thread.sleep(100);
return get(key, type, dbCallback);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback, Long expireTime, TimeUnit timeUnit) {
key = keyPrefix + key;
String json = stringRedisTemplate.opsForValue().get(key);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
return JSONUtil.toBean(json, type);
}
}
boolean isLock = tryLock(key);
if (isLock) {
try {
json = stringRedisTemplate.opsForValue().get(key);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
return JSONUtil.toBean(json, type);
}
}
T value = dbCallback.execute();
if (value == null) {
set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
} else {
set(key, value, expireTime, timeUnit);
}
return value;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(key);
}
} else {
try {
Thread.sleep(100);
return get(key, type, dbCallback, expireTime, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 逻辑过期解决缓存击穿问题
public <T> T getWithLogicalExpireTime(String key, Class<T> type, DbCallback<T> dbCallback, Long expireTime, TimeUnit timeUnit) {
key = keyPrefix + key;
LocalDateTime now = LocalDateTime.now();
String json = stringRedisTemplate.opsForValue().get(key);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);
if (redisData.getExpireTime().isBefore(now)) {
boolean isLock = tryLock(key);
if (isLock) {
String finalKey = key;
json = stringRedisTemplate.opsForValue().get(finalKey);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
if (redisData.getExpireTime().isAfter(now)) {
return JSONUtil.toBean(json, type);
}
}
}
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
T newData = dbCallback.execute();
if (newData == null) {
set(finalKey, nullValue, nullValueExpire, TimeUnit.SECONDS);
} else {
setWithLogicalExpireTime(finalKey, newData, expireTime, timeUnit);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(finalKey);
}
});
}
}
return data;
}
} else {
boolean isLock = tryLock(key);
if (isLock) {
try {
json = stringRedisTemplate.opsForValue().get(key);
if (json != null) {
if (json.equals(nullValue)) {
return null;
} else {
return JSONUtil.toBean(json, type);
}
}
T data = dbCallback.execute();
if (data == null) {
set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
} else {
setWithLogicalExpireTime(key, data, expireTime, timeUnit);
}
return data;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(key);
}
} else {
try {
Thread.sleep(100);
return getWithLogicalExpireTime(key, type, dbCallback, expireTime, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
public void del(String key) {
key = keyPrefix + key;
stringRedisTemplate.delete(key);
}
private boolean tryLock(String key) {
String lockKey = keyPrefix + "lock:" + key;
Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock");
return b != null && b;
}
private void unLock(String key) {
String lockKey = keyPrefix + "lock:" + key;
stringRedisTemplate.delete(lockKey);
}
public interface DbCallback<R> {
R execute();
}
@Setter
@Getter
private static class RedisData {
private Object data;
private LocalDateTime expireTime;
}
}