Redis缓存穿透、缓存击穿、缓存雪崩区别和解决方案

缓存穿透和缓存击穿是十分混淆的,以下对这两的定义有重点标注。很好区分。

  1. 缓存穿透: key对应的数据在redis缓存和数据库中都不存在,若不做任何处理,用户不断对这种key发起请求,请求直接打到数据库,造成数据库压力过大;
    比如一个不存在的商品id(-1)获取商品详情,数据库和redis缓存中都没有,每次请求就会去数据库中去查询,若被黑客利用进行疯狂请求攻击,极有可能压垮数据库。

代码示例及解决方法:

  • 方案一: 若key对应的数据在数据源中不存在,我们在redis缓存中存儲为空,这个空结果的过期时间尽量设置短些,最好不要超过五分钟,避免下次查询再次查询数据库。
// 示例代码
public Object getGoodsDetail(String goodsId){
	String goodsDetail = redisTemplate.opsForValue().get(goodsId);
	if (StringUtils.isBlank(goodsDetail )) {
		// 从数据库查询商品详情
		GoodsDetailVo goodsDetailVo = baseMapper.getGoodsDetailById(goodsId);
		// 不管goodsDetailVo 是否为空,都会存一份redis缓存,下次查询时就不会直接穿透到数据库了
		redisTemplate.opsForValue().set(goodsId, goodsDetailVo, 5*60);
		return goodsDetailVo;
	}
	return JSON.parseObject(goodsDetail , GoodsDetailVo .class);
}
  • 方案二: 布隆过滤器,利用redis的bigmap功能将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
    这也是一种常见的解决方案,但是相对第一种方案来说增加了代码的复杂度;布隆过滤器是利用hash算法将所有key匹配上对应的位置,若key存在则将对应位置上的值改为1表示该key存在,显然同一位置对应的key可以有多个,这个会导致有一定概率出现误判(bitmap长度越小,误判概率越高);同时由于同一位置存在多个key,所以在数据源删除了某一key值后,布隆过滤器无法删除之前对这个key是否存在的判定。
  1. 缓存击穿: 是指数据库有数据,但是redis缓存中没有数据,这时突然来了大量请求,直接请求到数据库,造成数据库压力过大。
  • 方案一: 使用互斥锁(mutex key):当发生缓存击穿时,只让一个线程去构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存中获取数据即可。

上面的代码就有这个问题,现在对上面代码进行改良

public Object getGoodsDetail(String goodsId){
	String goodsDetail = redisTemplate.opsForValue().get(RedisPrefixConstant.GOODS_DETAIL_PREFIX +goodsId);
	if (StringUtils.isBlank(goodsDetail )) {
		// 给查询数据库操作加上分布式锁,确保只有一个线程可以去操作数据库
		if(redisTemplate.opsForValue().setIfAbsent(goodsId, "1", 3 * 60 * 1000L, TimeUnit.MILLISECONDS)) {
			// 查询数据库
			GoodsDetailVo goodsDetailVo = baseMapper.getGoodsDetailById(goodsId);
			// 不管goodsVO是否为空,都会存一份redis缓存,下次查询的时候查询请求就不会直接穿透到数据库了
            redisTemplate.opsForValue().set(RedisPrefixConstant.GOODS_DETAIL_PREFIX + goodsId, JSON.toJSONString(goodsVO), 3 * 60 * 60 * 1000L);
            // 释放锁
            redisTemplate.opsForValue().delete(GOODS_MUTEX_PREFIX + lockKey)
		} else {
			// 再次从缓存中取,双重判断
            goodsDetail = redisTemplate.opsForValue().get(RedisPrefixConstant.GOODS_DETAIL_PREFIX + goodsId);
            if (ObjectUtil.isEmpty(goodsDetail)) {
                // 如果仍然为空,则增加延迟后,递归获取
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return searchGoodsById(goodsId);
            }
		}
	}
	return JSON.parseObject(goodsDetail , GoodsDetailVo .class);
}
  • 方案二: 使用多级缓存
    redis一级缓存的过期时间timeout1,要比二级缓存memcache timeout2要短,当读取redis缓存时,发现其已经过期,马上查询二级缓存,并同时查询数据库将新的数据缓存起来。

  • 方案三: 设置永远不过期
    (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
    (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

  1. 缓存雪崩: 当缓存服务器重启或者大量缓存集中在某一个时间段失效,这时候大量请求进来,大量请求数据库,也会给数据库带来很大压力而导致宕机。
  • 方案一:规划过期时间 避免缓存设置相近的有效期;为有效期增加随机值;统一规划有效期,失效时间均匀分布。

  • 方案三:二级缓存 主缓存:有效期按照经验值设置,主要读取的缓存,主缓存失效后从数据库加载最新值。
    备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。

  • 方案二:不设置过期时间 设置缓存不过期或者缓存异步更新,这样就不会存在缓存失效问题,但是不保证一致性,代码复杂度增大(每个value值都要维护异步更新代码),容易堆积垃圾数据。

对于上面Redis所有可能存在的问题,有一道基础屏障,那就是接口做限流、熔断、降级等处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值