持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
1、缓存穿透
缓存穿透是指客户端请求的数据 在缓存中和数据库中 都不存在,这样缓存永远不会生效,这些请求都会请求到数据库
解决方案:
- 缓存空对象
- 优点:实现简单,维护方便
- 缺点:额外的内存消耗,可能造成短期的不一致问题
- 布隆过滤:内存占用较少,没有多余key;实现复杂,存在误判可能
改进方案
对于前面查询商户信息的功能,现在做出改进
1. 若缓存没有命中,从数据库中查询获得店铺信息,若数据库中存在,则写入redis缓存中;若数据库中不存在,则将 null 空值写入 redis中,最后返回 2. 若缓存命中,此时也会分为两种情况,从缓存中查询到的是否为空值,若为空值,直接返回;若不为空值,返回店铺信息
@Override
public Object findShopById(Long id) {
// 先从redis中查询是否存在该商铺
String shopValue = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(shopValue)) {
JSON json = JSONUtil.parse(shopValue);
return Result.ok(json);
}
if (shopValue != null) {
// 表示是一个空值
return Result.fail("店铺信息不存在");
}
// 若不存在,从mysql中查询,写入 redis 中,再返回
Shop shop = this.getById(id);
// 从数据库中查询得到空值,写入 空值 到redis中
if (shop == null) {
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
String jsonStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
缓存穿透产生的原因是什么?
- 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
2、缓存雪崩
缓存雪崩是指在同一时间段内大量的key同时失效或者redis服务宕机,导致大量的请求到达数据库
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
3. 缓存击穿
缓存击穿:也称为 热点key 问题,就是一个高并发访问并且缓存重建业务较复杂的key突然失效,无数的请求访问会瞬间请求数据库,给数据库造成压力
当多线程同时请求的时候,都会先去查询缓存,若都未命中缓存,都会去查询数据库
解决方案:
- 互斥锁:当线程1请求缓存,未命中,给线程1加互斥锁,再去查询数据库,然后写入缓存,最后释放锁;那么当其他线程未命中缓存,那么也会去获取锁,由于锁是互斥的,那么会获取失败,然后然线程2休眠重试,重新查询缓存
- 逻辑过期:在缓存中手动存入key过期的时间字段,如下格式
| key | value |
|---|---|
| heima:user:1 | {name:"Jack", age:21, expire:152141223} |
线程1查询缓存,未命中缓存,获取互斥锁,然后开启一个新的线程2,由线程2异步的执行查询数据库重建缓存数据,写入缓存,重置逻辑过期时间,最后线程2释放互斥锁;此时线程1开启新线程之后,直接返回过期数据;当由线程3来请求缓存的时候,未命中缓存或缓存已过期,去获取互斥锁,失败,返回过期的数据;可能存在线程4来请求缓存,这个时候就可能命中缓存,并且没有过期
由于加锁会影响性能,线程会等待,性能会受影响
3.1 缓存击穿解决方案:基于互斥锁
具体流程:若缓存未命中,先尝试获取互斥锁,若锁获取成功,在去数据库查询,通过将获取的数据写入redis,再释放锁,返回数据;若互斥锁获取失败,休眠一段时间,重新查询缓存
redis 加锁的命令: (String类型) setnx stringRedisTemplate 中方法 setIfAbsent
public Shop queryWithPassThrough(Long id) {
// 先从redis中查询是否存在该商铺
String shopValue = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(shopValue)) {
return JSONUtil.toBean(shopValue, Shop.class);
}
if (shopValue != null) {
// 表示是一个空值
return null;
}
Shop shop = null;
try {
// 1. 从redis中未获取到缓存,则先获取互斥锁
boolean flag = tryLock("lock:shop:" + id);
if (!flag) {
// 若获取互斥锁失败,则休眠,重新获取缓存
Thread.sleep(50);
queryWithPassThrough(id);
}
// 2. 再去缓存查询
// 若不存在,从mysql中查询,写入 redis 中,再返回
Shop shop = this.getById(id);
// 从数据库中查询得到空值,写入 空值 到redis中
if (shop == null) {
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
String jsonStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (Exception e) {
} finally {
// 3. 最后释放锁
unLock("lock:shop:" + id);
}
return shop;
}
注意:由于代码可能任何会获取锁失败,或者其他原因报错,那么最终都应该释放锁,所有释放锁的代码写在 finally 代码块中