1.缓存穿透
原因:恶意攻击一个缓存和数据库都不存在的key,使数据库处理大量请求,增大数据库压力。
解决方案:
- 缓存null值:当查询到数据库为空时,在缓存中写入一个null值,使后续请求命中缓存。缺陷:缓存压力增大,一旦数据库中真的存在了这个key,则出现缓存与数据库不一致问题。
- 布隆过滤器:在缓存预热时设置布隆过滤器,请求先到达布隆过滤器,非法请求将直接拦截。
注意:在新增和删除数据时,布隆过滤器也要随之进行更新!
缓存预热:
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IShopService shopService;
// 预期插入数量
static long expectedInsertions = 200L;
// 误判率
static double falseProbability = 0.01;
private RBloomFilter<Long> bloomFilter;
@Resource
private BloomFilterUtil bloomFilterUtil;
@Override
public void afterPropertiesSet() throws Exception {
//缓存预热
//1.查询热点数据
List<Shop> list = shopService.list();
//2.设置布隆过滤器
bloomFilter = bloomFilterUtil.create("shopBloomFilter", expectedInsertions, falseProbability);
//3.添加缓存
for (Shop shop : list) {
redisTemplate.opsForValue().set("shop", JSONUtil.toJsonStr(shop));
bloomFilter.add(shop.getId());
}
}
}
public Result queryByIdBloom(Long id){
//1.通过布隆过滤器
if (!bloomFilter.contains(id)){
//2.非法查询,返回空
return Result.fail("shop not exist!");
}
//TODO 后续业务逻辑
return null;
}
布隆过滤器实现原理:
bitmap:是一个以bit为单位的数组,数组中每个单元只能存储二进制数0或1。
布隆过滤器作用:可以检索一个元素是否存在集合中。
误判情况:
误判率:数组越大误判率越小,数组越小误判率越大。误判率是肯定会存在的,我们一般可以设置误判率,不要超过5%。项目上也可以接受。
2.缓存击穿
原因:当给某一个key设置了过期时间,当key过期的时候恰好又有大量请求并发进来直接命中数据库,可能会直接把数据库压垮。
解决方案:
-
互斥锁:当线程1查询缓存未命中时,获取互斥锁,查询数据库重建缓存。此时其他线程进来获取锁失败,不断重试直到命中缓存。(强一致性,性能稍差一些)
@Retryable(value = {RedisNxException.class}, maxAttempts = 3) public Result queryByIdNx(Long id) throws Exception { //1.先查找缓存 String cacheShop = redisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); if (cacheShop == null) { //2.缓存未命中,获取互斥锁重建缓存 if (getLock("shopnx" + id)) { //3.获取锁成功,重建缓存 Shop shop = shopMapper.selectById(id); redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); //4.释放锁 deleteLock("shopnx" + id); //5.返回 return Result.ok(shop); } else { //获取锁失败,等待重试 Thread.sleep(500); //当方法抛出异常时,会重试 throw new RedisNxException("获取锁失败,等待重试"); } } //缓存命中直接返回 return Result.ok(cacheShop); }
-
逻辑过期:在设置缓存时设置逻辑过期时间,当线程1查询缓存发现逻辑已过期时,获取锁,开启新线程重建缓存,之后返回过期数据。其他线程获取锁失败后直接返回过期数据。(性能好,数据稍有不一致)
public Result queryByIdExpire(Long id) { //1.先查找缓存 String cacheShop = redisTemplate.opsForValue().get(CACHE_SHOP_KEY + id); RedisData redisData = JSONUtil.toBean(cacheShop, RedisData.class); if (redisData.getExpireTime().isBefore(LocalDateTime.now())) { //2.发现缓存逻辑已过期,获取互斥锁,开启新线程重建缓存 if (getLock("shopnx" + id)) { //3.获取锁成功,开启新线程重建缓存 CACHE_REBUILD_EXECUTOR.submit(() -> { try { //4.重建缓存 Shop shop = shopMapper.selectById(id); String jsonStr = JSONUtil.toJsonStr(new RedisData(LocalDateTime.now().plusDays(1L), shop)); redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr); } catch (Exception e) { ShopServiceImpl.log.info(e.getMessage()); } finally { //5.缓存重建完毕,释放锁 deleteLock("shopnx" + id); } }); //返回旧数据 return Result.ok(redisData.getObject()); } else { //获取锁失败,返回旧数据 return Result.ok(redisData.getObject()); } } //未过期直接返回 return Result.ok(redisData.getObject()); }
3.缓存雪崩
原因:大批热点数据同时失效,导致大量请求同一时间直达数据库,造成数据库崩溃。
解决方案:
- 给相同过期时间的key添加随机TTL。
- 给缓存业务添加限流降级策略。
- 给业务添加多级缓存。(Guava)