redis缓存穿透、击穿、雪崩解决方案

1.缓存穿透

缓存穿透:key对应的数据在数据库不存在,每次针对此key的请求从缓存获取不到,请求都送到数据库,从而可能压垮数据库。比如用一个用户id获取用户信息,一般情况是先从缓存中查询,如果缓存没有数据,那么会往DB进行查询,DB查询出数据,则会将数据库放入缓存,如果没有就不会放入缓存,如果有人恶意用一个不可能存在的用户id获取数据,那么可能压垮数据库。
##解决办法:

  • 1.设定正则过滤。对于每一个缓存key都有一定的规范约束,这样在程序中对不符合parttern的key的请求可以拒绝。
  • 2.使用bitmap(布隆过滤器)。将可能出现的缓存key的组合方式的所有数值以hash形式存储在一个很大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,在java中可以理解为一个hashset。

  • 3.将查询空值放入redis(常用)。如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值,比如“NULL”,并设置一个缓存的失效时间,避免类似的数据太多,对于redis造成一定压力。这个key的时效比正常的时效要小的多,一般将过期时间设定在五分钟以内。
/**
 * @description:避免缓存穿透的伪代码
 * @param key 要查询的key
 */
public String getValue(String key) {
	int cacheTime = 5 * 60 * 1000;//为空时候设置的过期时间
	String value = redisUtil.getKey(key);
	if (null != value) {//缓存不为空,则返回
		return value;
	}else {
		//缓存为空,则向DB查询
		value = curdUtil.getData();
		if(value == null) {
			value = "";
			redisUtil.set(key,value,cacheTime);//将为空的值放入并设置过期时间
			return value;
		}
		redisUtil.set(key,value);
	}
	return value;
}

2.缓存击穿

缓存击穿:热点key在某个特殊的场景时间内恰好失效了(例如到期了),恰好有大量并发请求过来了,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
##解决办法:

  • 1.使用互斥锁(mutex key),较常用。在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

/**
 * @description:避免缓存击穿的伪代码
 * @param key 要查询的key
 */
public String getValue(String key) {
	String value = redisUtil.get(key);
	if(null == value) {//代表缓存过期了
		//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
		String keyMutex = key+"_mutex";
		if (redisUtil.setnx(keyMutex, 1, 3 * 60) == 1) {  //代表设置成功
			value = curdUtil.get(key);
			redisUtil.set(key, value, expire_secs);
			redisUtil.del(key_mutex);
		}else {//这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
			 sleep(50);
			 value = redisUtil.get(key);;  //重试
		}
	}
	return value;
}

3.缓存雪崩

缓存雪崩:与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key。

缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

1.随机值

/**
 * @description:避免缓存雪崩的随机值伪代码
 * @param key
 */
public Object getValue(String key) {
	int cacheTime = 20;// 过期时间
	String cacheSign = key + "_sign";// 缓存标记
	String sign = redisUtil.get(cacheSign);
	// 获取缓存值
	String value = redisUtil.get(key);
	if (null != sign) {
		return value; // 未过期,直接返回
	} else {
		redisUtil.set(cacheSign, "1", cacheTime);
		ThreadPool.QueueUserWorkItem((arg) -> {
			// 向数据库查询
			value = curdUtil.get(key);
			// 日期设缓存时间的2倍,用于脏读
			redisUtil.set(key, value, cacheTime * 2);
		});
	}
	return value;
}

解释说明

  • 1.缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
  • 2.缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

2.加锁排队

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

/**
 * @param key
 * @description:避免缓存雪崩的随机值伪代码
 */
public Object getValue(String key) {
	int cacheTime = 20;// 过期时间
	String lockKey = key;
	String value = redisUtil.get(key);
	if(null != value) {
		return value;
	}else {
		synchronized (lockKey) {
			value = redisUtil.get(key); 
			if(null != value) {
				return value;
			}else {
				value = curdUtil.getData();
				redisUtil.set(key,value,cacheTime);
			}
		}
	}
	return value;
}

解释说明

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

3.二级缓存

待调研

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值