缓存穿透
- 指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库
- 一般都是不怀好意的人故意对系统进行攻击引发的缓存穿透
- 常见解决方案一:当数据库查询不存在时缓存一个空对象。这样会造成额外的内存消耗,可以通过设置 TTL 时间缓解
- 常见解决方案二:布隆过滤器。内存占用较少,但实现复杂,且存在误判可能
- 上述两个都是被动方案,我们也可以主动的防止被缓存穿透
- 增加 id 复杂度,避免被猜中,然后进行 id 的校验,不满足条件直接 pass
- 加强用户的权限校验,限制用户访问接口的次数
- 做好热点参数的限流
通过空对象方式解决商铺信息缓存穿透问题
/**
* 根据id查询商铺信息
*
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// 1. 查询 redis 商铺缓存
String redisShopKey = "cache:shop" + id;
String shopJson = stringRedisTemplate.opsForValue().get(redisShopKey);
// 2. 查到了,直接返回
if (StrUtil.isNotBlank(shopJson)) {
return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
}
// 3. 查不到
// 3.1 如果查不到时并不是 null ,说明是我们缓存的空值,直接返回错误信息
if (null != shopJson) {
return Result.fail("店铺不存在!");
}
// 3.2 查不到时是 null ,继续查询数据库
Shop shop = shopMapper.selectById(id);
// 4. 查不到
if (shop == null) {
// 4.1 将空值写入 redis
stringRedisTemplate.opsForValue().set(redisShopKey, "", 1L, TimeUnit.MINUTES);
// 4.2 返回错误信息
return Result.fail("店铺不存在!");
}
// 5. 查到了
// 5.1 写入 redis
stringRedisTemplate.opsForValue().set(redisShopKey, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
// 5.2 返回数据
return Result.ok(shop);
}
缓存雪崩
- 是指在同一时段内大量的缓存同时失效或 Redis 服务宕机,导致大量请求直接到达数据库,带来巨大压力
- 常见解决方案一:对热点数据进行缓存预热,并给不同的 key 设置 TTL 随机值,避免大量缓存同时失效
- 常见解决方案二:利用 Redis 集群提高服务的可用性
缓存击穿
- 也叫热点 Key 问题。指的是一个被高并发访问的 key 由于各种原因(数据更新、过期等)被删除了,在缓存重建期间无数请求瞬间给数据库带来巨大的冲击
- 常见解决方案一:提前对热点数据进行缓存,并设置永不过期
- 常见解决方案二:互斥锁。保证了一致性,但线程需要等待,牺牲了一定的可用性
通过互斥锁方式解决商铺信息缓存击穿问题
- 上面的流程图不全,我们需要在获取锁之后从 Redis 中查询商铺缓存并判断是否命中,未命中再继续,命中直接返回
- 我们需要根据获取锁与否做不同的操作,java 的锁在获取不到的情况下只会等待,所以我们需要其他方式实现互斥锁
- 互斥锁说白了就是只有一个线程可以获取到,其他线程在释放锁之前只能等待,我们可以利用 Redis 的 setnx 实现这种效果
- setnx 就是当 key 不存在时才能插入,我们随意定义一个 key ,获取锁就相当于第一个插入该 key ,锁的释放就是删除该 key
/**
* 基于 setnx 实现互斥锁
*
* @return 是否获取到互斥锁
*/
private boolean getLock(String key) {
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 5, TimeUnit.SECONDS);
// 这里不能直接返回,会自动拆箱,如果为 null 会报空指针异常
return BooleanUtil.isTrue(aBoolean);
}
/**
* 释放 setnx 锁
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
/**
* 根据id查询商铺信息
*
* @param id 商铺id
* @return 商铺详情数据
*/
@Override
public Result queryById(Long id) {
// 1. 查询 redis 商铺缓存
String redisShopKey = "cache:shop" + id;
String shopJson = stringRedisTemplate.opsForValue().get(redisShopKey);
// 2. 查到了,直接返回
if (StrUtil.isNotBlank(shopJson)) {
return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
}
// 3. 查不到
// 3.1 如果查不到时并不是 null ,说明是我们缓存的空值,直接返回错误信息
if (null != shopJson) {
return Result.fail("店铺不存在!");
}
// 3.2 查不到时是 null ,尝试获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop;
try {
boolean isLock = getLock(lockKey);
// 3.3 如果没获取到,则休眠 50ms 并重试
if (!isLock) {
Thread.sleep(50);
queryById(id);
}
// 3.4 获取到
// 3.4.1 查询 redis 商铺缓存,查到了直接返回
if (StrUtil.isNotBlank(stringRedisTemplate.opsForValue().get(redisShopKey))) {
return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
}
// 3.4.2 没查到,继续查询数据库
shop = shopMapper.selectById(id);
// 4. 查不到
if (shop == null) {
// 4.1 将空值写入 redis
stringRedisTemplate.opsForValue().set(redisShopKey, "", 1L, TimeUnit.MINUTES);
// 4.2 返回错误信息
return Result.fail("店铺不存在!");
}
// 5. 查到了,写入 redis
stringRedisTemplate.opsForValue().set(redisShopKey, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 6. 释放锁(释放资源的操作要放入 finally)
unLock(lockKey);
}
// 7. 返回数据
return Result.ok(shop);
}