【Redis】缓存和分布式锁

目录

一、缓存

1、缓存使用

[高并发下缓存失效问题 - 缓存穿透(查询一个不存在的数据,缓存与数据库中都没有)](#高并发下缓存失效问题 - 缓存穿透(查询一个不存在的数据,缓存与数据库中都没有))

[高并发下缓存失效问题 - 缓存雪崩(大面积数据 key 同时失效,过期时间相同)](#高并发下缓存失效问题 - 缓存雪崩(大面积数据 key 同时失效,过期时间相同))

[高并发下缓存失效问题 - 缓存击穿(高并发访问一个刚好失效的热点 key)](#高并发下缓存失效问题 - 缓存击穿(高并发访问一个刚好失效的热点 key))

二、分布式锁

一、什么是分布式锁

二、Redis分布式锁原理

三、引入过期时间

四、引入校验id

五、引入Lua

六、引入看门狗

七、引入RedLock算法

一、缓存

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完成同步锁的流程及事项如下:

  1. 使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

  2. 为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个"合理"的过期时间

  3. 释放锁,使用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节点,而要写多个

相关推荐
NPE~2 小时前
[docker/大数据]Spark快速入门
大数据·分布式·docker·spark·教程
柯南二号5 小时前
【Java后端】【可直接落地的 Redis 分布式锁实现】
java·redis·分布式
卑微的小鬼5 小时前
如何保证数据库和缓存的一致性?
数据库·缓存
原来是好奇心5 小时前
用户登录Token缓存Redis实践:提升SpringBoot应用性能
spring boot·redis·缓存
wuyunhang1234567 小时前
Redis---事务
数据库·redis·缓存
Tacy02137 小时前
Redis 安装教程
数据库·redis·缓存
用手编织世界9 小时前
redis-缓存-双写一致性
数据库·redis·缓存
Rookie小强11 小时前
kafka的rebalance机制是什么
分布式·kafka
终端行者11 小时前
jenkins实现分布式构建并自动发布到远程服务器上 jenkins实现自动打包编译发布远程服务器
服务器·分布式·jenkins