文章目录
- 1、缓存击穿的概念以及原因
- 2、预防缓存击穿的思路
- 3、解决方案一:互斥锁 (mutex key)
- 3-1、具体做法
- 3-2、风险
- 4、解决方案二:只做逻辑过期
- 5、一个小的处理技巧
1、缓存击穿的概念以及原因
给缓存中的数据添加过期时间,既可以加速数据读写,又能够保证数据定期更新。
但是在一些场景下数据过期会给系统造成重大伤害:
- 条件1:该数据为热点内容,并发读取量非常大。
- 条件2:重建缓存无法在短期内完成。比如重新计算/获取该数据是一个复杂的过程,涉及到复杂SQL、耗时的各种IO或者依赖很多个其他三方服务等等。
在热点数据过期的瞬间,大量并发的请求来获取数据,获取不到以后,会引发大量的重建缓存的动作,造成存储层瞬时负载激增,系统崩溃。
2、预防缓存击穿的思路
尽最大可能减少重建缓存的次数,将大量请求隔离在存储层之外。
3、解决方案一:互斥锁 (mutex key)
此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完成后,才可以从缓存获取数据。
3-1、具体做法
流程以及伪代码如下:
String value = redis.get(热点数据_key);
// value为空的话表明缓存已过期
if (value == null) {
// 尝试获取分布式锁。注意锁本身要加过期时间,否则一旦出现各种不可预料的异常,锁会长期存在
if ("OK".equals(jedis.set(互斥锁_key, uniqueId, "NX", "EX", 互斥锁_expire_secs))) {
value = 重建缓存;
redis.set(热点数据_key, value, 热点数据_expire_secs);
// 锁解除(删除互斥锁)
if (uniqueId.equals(jedis.get(互斥锁_key))) {
jedis.del(互斥锁_key);
}
} else {
// 获取锁失败,说明已经有其他线程在重建缓存
线程等待 & 重试;
}
} else {
return value;
}
注意一下加锁和释放锁时的正确操作姿势。
加锁时,Redis 2.6.12 以后的版本直接使用set命令即可:
SET key value [EX seconds][PX milliseconds][NX|XX]
- EX seconds:设置指定的过期时间,单位秒。
- PX milliseconds:设置指定的过期时间,单位毫秒。
- NX:仅当key不存在时设置值。
- XX:仅当key存在时设置值。
释放锁时,养成判断value的习惯,不怕一万就怕万一,万一这把锁不是你加的呢?所以判断一下锁的value是不是你设定的那个随机数,看看这把锁是不是属于你。
实际上上面伪代码中释放锁的姿势也不是非常正确,毕竟get和del是两个操作,不像set一样具备原子性。如果追求完美的话,可以写lua脚本:
/**
* 释放分布式锁
* @param key
* @param uniqueId
*/
public static boolean releaseLock(String key, String uniqueId) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(uniqueId)).equals(1L);
}
3-2、风险
使用互斥锁来阻塞读的方案,思路简单实现容易,但是存在一定的隐患。
由于它会阻塞其他的线程,会使得系统吞吐量会下降,需要结合实际的业务去考虑是否要这么做。
另外如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,反而拖累系统的整体表现。
4、解决方案二:只做逻辑过期
Redis 层面,热点数据不再设置过期时间,永远有效,这样绝对不会出现缓存击穿的现象,所有的访问可以随时获取到结果。
从业务功能层面,需要单独管理每一个热点数据需要刷新的时间,也就是原来的数据过期时间,利用批处理等手段(或者守护线程),在这个“逻辑过期”时间到了以后,去重新构建缓存中的热点数据。
有一些解决方案中提到设定一个缓存过期时间,一个数据过期时间,实际上跟这种“永不过期”的方案思路一致,但是实现反而更复杂了,可靠性也有所降低。
5、一个小的处理技巧
给热点数据设定过期时间的时候,增减一个小的随机数,可以防止大量热点数据同时失效,让它们的失效时间相互错开。
比如
5s(过期时间) ± 0.076s(随机数)