1.缓存更新策略
主动更新用的最多。
主动更新一般是由缓存的调用者,在更新数据库的同时,更新缓存。
操作缓存和数据库时有三个问题需要考虑:
- 删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存 - 如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案 - 先操作缓存还是先操作数据库?
先操作数据库,再删除缓存
先删除缓存,再操作数据库
2.缓存穿透
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
如下图所示,这里基于缓存空对象实现:
- 缓存穿透代码
// 封装了缓存穿透的处理
/*
参数里面需要传递查询数据库的函数
*/
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit,
Function<ID, R> dbFallback) {
// 1.从Redis查询商铺缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在则返回缓存数据
return JSONUtil.toBean(json, type);
}
// 命中是否为空的缓存
if(json != null){
return null;
}
// 4.不存在则查询数据库
R r = dbFallback.apply(id);
// 5. 数据库不存在
if (r == null) {
// 将空值保存在redis中,应对缓存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6. 数据库存在,写入缓存
this.set(key, r, time, unit);
return r;
}
- 数据写入缓存代码
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
3. 缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
4.缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
互斥锁
// 互斥锁解决缓存击穿
public <R,ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFallback) {
// 1.从Redis查询商铺缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在则返回缓存数据
return JSONUtil.toBean(json, type);
}
// 命中是否为空的缓存
if(json != null){
return null;
}
// 4. 实现缓存重建
// 4.1 尝试获取分布式锁
String lockKey = "lock:shop:" + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 判断是否获取成功
if (!isLock) {
// 4.3 失败,则失眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, time, unit, dbFallback);
}
// 4.4 成功则查询数据库
r = dbFallback.apply(id);
// 5. 数据库不存在
if (r == null) {
// 将空值保存在redis中,应对缓存穿透
this.set(key, "", time, unit);
return null;
}
// 6. 数据库存在,写入缓存
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7. 释放锁
releaseLock(lockKey);
}
return r;
}
- 锁相关代码
// 线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 尝试获取分布式锁
private boolean tryLock(String key) {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 这样更安全
return BooleanUtil.isTrue(lock);
}
// 释放分布式锁
private void releaseLock(String key) {
stringRedisTemplate.delete(key);
}
逻辑过期
- 逻辑过期存入数据
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));
}
- 逻辑过期锁查询数据
public <R,ID> R queryWithLogicalExpire(String keyPrefix ,ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFallback) {
// 1.从Redis查询商铺缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.不存在
return null;
}
// 4. 命中,json反序列化对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1 未过期直接返回
return r;
}
// 5.2 过期则异步更新缓存
// 6. 缓存重建
// 6.1 尝试获取分布式锁
String lockKey = LOCK_SHOP_KEY + id;
// 6.2 判断是否获取成功
if (tryLock(lockKey)) {
// 6.3 成功,开启独立线程实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
// 重建缓存
try {
// 查数据库
R r1 = dbFallback.apply(id);
// 保存在redis中
this.setWithLogicalExpire(key, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
releaseLock(lockKey);
}
});
}
// 6.4 返回过期的信息
return r;
}