目录
[高并发下缓存失效问题 - 缓存穿透(查询一个不存在的数据,缓存与数据库中都没有)](#高并发下缓存失效问题 - 缓存穿透(查询一个不存在的数据,缓存与数据库中都没有))
[高并发下缓存失效问题 - 缓存雪崩(大面积数据 key 同时失效,过期时间相同)](#高并发下缓存失效问题 - 缓存雪崩(大面积数据 key 同时失效,过期时间相同))
[高并发下缓存失效问题 - 缓存击穿(高并发访问一个刚好失效的热点 key)](#高并发下缓存失效问题 - 缓存击穿(高并发访问一个刚好失效的热点 key))
一、缓存
1、缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作(持久化工作),数据库查询一次后将数据存入缓存,以后需要该数据直接从缓存中取。
适合放入缓存的数据:
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少
缓存中存放的所有对象都应该是 JSON 字符串,JSON 跨语言、跨平台兼容。
给缓存中存放 JSON 字符串,从缓存中拿出的 JSON 字符串,还要逆转为能用的对象类型【序列化与反序列化的过程】
高并发下缓存失效问题 - 缓存穿透(查询一个不存在的数据,缓存与数据库中都没有)
缓存穿透:
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:
null 结果缓存,并加入短暂过期时间
高并发下缓存失效问题 - 缓存雪崩(大面积数据 key 同时失效,过期时间相同)
缓存雪崩:
指在我们设置缓存时 key 采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重导致雪崩
解决:
原有的失效时间基础上增加一个随机值,比如 1~5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
高并发下缓存失效问题 - 缓存击穿(高并发访问一个刚好失效的热点 key)
缓存击穿:
- 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常"热点"的数据
- 如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 DB,这就是缓存击穿
解决:
加锁 ------ 大量并发只让一个人去查,其它人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用再去查 DB
二、分布式锁
一、什么是分布式锁
分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
一把靠谱的分布式锁应该具有以下特征:

二、Redis分布式锁原理
锁的实现主要基于redis 的SETNX(SET if Not eXists 如果不存在,则 SET)命令
SETNX key value将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。【设置成功,返回 1; 设置失败,返回 0】。
使用SETNX完成同步锁的流程及事项如下:
-
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
-
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个"合理"的过期时间
-
释放锁,使用DEL命令将锁数据删除
三、引入过期时间
当服务器1加锁之后,开始处理的过程中,如果服务器1意外宕机了,就会导致解锁操作(删除该 key)不能执⾏.就可能引起其他服务器始终⽆法获取到锁的情况. 为了解决这个问题,可以在设置key的同时引⼊过期时间.即这个锁最多持有多久,就应该被释放.
可以使用set ex nx在设计锁的同时设置过期时间
四、引入校验id
在 Redis 中写入的加锁键值对,其他节点也可以删除。比如服务器 1 写入一个 "001": 1 这样的键值对,服务器 2 是完全可以把 "001" 给删除掉的。当然,服务器 2 不会进行这样的 "恶意删除" 操作,不过不能保证因为一些 bug 导致服务器 2 把锁误删除。为了解决上述问题,我们可以引入一个校验 id。比如可以把设置的键值对的值,不再是简单的设为一个 1,而是设成服务器的编号,形如 "001": "服务器 1"。这样就可以在删除 key(解锁)的时候,先校验当前删除 key 的服务器是否是当初加锁的服务器,如果是,才能真正删除;不是,则不能删除。
String key = [要加锁的资源 id];
String serverId = [服务器的编号];
// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");
// 执⾏各种业务逻辑, ⽐如修改数据库数据.
doSomeThing();
// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配.
if (redis.get(key) == serverId) {
redis.del(key);
}
五、引入Lua
为了使解锁操作原子,可以使用Redis的lua脚本功能
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
上述代码可以编写成一个.Lua后缀的文件,由redis-cli等客户端加载并且发送Redis服务器,由Redis服务器执行这个逻辑,一个Lua脚本会被Redis服务器以原子的方式执行
六、引入看门狗
上述的方案仍然存在一个重要的问题,当我们设置了key过期时间后,有一定的可能性,当任务没有执行完,key就先过期了,这就导致锁提前失效
所谓的watch dog,本质上是加锁的服务器的一个单独的线程,通过线程来对锁的过期时间来进行续约
举个例子:
初始情况下设置的过期时间为10s,同时设定看门狗线程每隔3s检测一次
那么当3s时间到的时候,看门狗就会判定当前任务是否完成
如果已经完成,则直接通过lua脚本的方式释放锁
如果任务未完成,则把过期时间重写设置为10s
七、引入RedLock算法
实践中的Redis一般是以集群的方式部署的
服务器1向master节点进行加锁的操作,这个写入key的过程刚刚完成,master挂了;slave节点升级成了新的master节点,但是由于刚才写入的key尚未来得及同步给slave,此时就相当于服务器1假的锁形同虚设了,服务器2仍然可以进行加锁
为了解决这个问题,引入了RedLock算法
我们引入一组Redis节点,其中每一组Redis节点都包含一个主节点和若干个从节点,并且存储的数据都是一致的,相互之间是备份关系,加锁的时候按照一定的顺序,写入多个master节点,设定超时时间
简而言之,RedLock算法核心是加锁操作不能只写给一个Redis节点,而要写多个