缓存
缓存穿透¶
定义¶
客户端请求的数据在缓存中和数据库中都不存在,使得请求全部打到数据库
解决方案¶
-
缓存空对象
将不存在的键值对缓存到 redis 中(val 为 null)
- 额外的内存消耗(通过加 TTL 缓解)
- 可能存在短期的不一致性,当数据库中存在之后缓存中仍然为 null(通过控制 TTL 的时间或主动覆盖缓解)
-
布隆过滤
在客户端和 Redis 之间加一个布隆过滤器
布隆过滤器使用 bitmap 来实现,不是百分百准确:若判断不存在则一定不存在,若判断存在则可能不存在,依然有穿透的风险
实现¶
缓存空对象¶
缓存雪崩¶
定义¶
同一时段:
- 大量的缓存 key 同时失效
- Redis 服务宕机
解决方案¶
- 将不同的 key 的 TTL 设置为随机的
- 利用 Redis 集群提高服务的可用性
- 给缓存业务添加降级限流策略(快速失败,拒绝服务等)
- 给业务添加多级缓存
缓存击穿¶
定义¶
即热点 key 问题,一个被高并发访问并且缓存重建业务较为复杂的 key 突然失效了,无数请求同时访问数据库带来巨大的冲击
- 高并发访问:比如某个正在促销的商品
- 缓存重建业务较复杂:重新从数据库查询时,需要进行多表查询或者进行表关联的运算
多个请求同时对数据库进行高成本操作:
解决方案¶
注意这里的查询函数是对 shop 基于 id 的查询,这里的热 key 为 shop
互斥锁
- 加锁是只让最先的请求去把数据从数据库中取到 redis 中
逻辑过期
在插入数据之外还需要手动插入一个 expire_time,即插入一个逻辑过期的时间
- 此时锁是用来防止重复创建查询线程的
优缺点对比¶
- 互斥锁选择了一致性
- 逻辑过期使用了可用性
实现¶
互斥锁¶
通过 Redis 的 setnx 来实现互斥锁,当不同的 JVM 中不同的线程拿到锁之前都需要在同一个 redis 中执行 setnx,只有此命令执行成功才算获取锁成功。
自定义 tryLock 方法
private boolean tryLock(String key)
{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); // 方法的返回类型是 Boolean
return BooleanUtil.isTrue(flag);
}
private void unlock(String key)
{
stringRedisTemplate.delete(key);
}
业务逻辑
public Shop queryWithMutex(Long id)
{
String key CACHE_SHOP_KEY + id;
/* 从 redis 中查询 */
String shopJson = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(shopJson)){ // 不是 null 或 "" 等,即命中了缓存
return JSONUtil.toBean(shopJson, shop.class);
}
if(shopJson == null){
return null // 是为了解决缓存穿透所缓存的空对象
}
/* 从数据库中查询 */
String lockKey = "lock:shop:" + id;
boolean isLock = tryLock(lockKey);
// 获取锁失败,先休眠,再递归调用查询方法
if(!isLock){
Thread.sleep(50);
queryWithMutex(id);
}
Shop shop = getById(id); // getById() 查询 MySQL
if(shop == null){ // 没有在数据库中查到,缓存空对象以应对缓存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, T);
retrun null;
}
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MININTES);
unlock(lockKey); // 放锁
return shop;
}
逻辑过期¶
这里假设热 key 已经添加到了 redis 中,那么缓存理论上不会出现未命中的情况;但是为了健壮性,这里还是加上了一个未命中直接返回 null 的逻辑
业务逻辑
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire(Long id)
{
String key CACHE_SHOP_KEY + id;
/* 从 redis 中查询 */
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 缓存未命中
if(StrUtil.isBlank(shopJson)){
return null;
}
/* 缓存命中,判断逻辑过期 */
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); // 把 json 反序列化为对象
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class); // 直接查旧数据
LocalDataTime expireTime = redisData.getExpireTime();
// 逻辑未过期
if(expireTime.isAfter(LocalDataTime.now())){
return shop
}
// 逻辑已过期
String lockKey = "lock:shop:" + id;
boolean isLock = tryLock(lockKey);
// 成功获取锁,创建子线程从数据库中读取
if(isLock){
CACHE_REBUILD_SERVICE.submit(() -> {
this.saveShop2Redis(id, 20L); // 重新从数据库读
unlock(lockKey);
})
}
// 直接返回旧数据
return shop;
}