Redis分布式锁

引入分布式锁

我们之前学过的锁,本质上都是只能在一个进程内部生效

在分布式系统中,是有很多进程的(每个服务器,都是独立的进程)

因此,之前的锁,难以对现在分布式系统中的多个进程之间产生制约

并且分布式系统中,多个进程之间的执行顺序也是不确定的=>随机性

引入"分布式锁"来解决上述问题

分布式锁的基础实现

举个例子:

客户端1,先执行查询余票,发现剩余1张

在即将执行1->0过程之前

客户端2也执行查询余票,发现剩余1张

客户端2也会执行1->0过程

所谓的分布式锁,也是一个/一组单独的服务器程序,给其他的服务器提供"加锁"这样的服务

(redis是一种典型的可以用来实现分布式锁的方案,但不是唯一的一种,也可以使用mysql/zookeeper这样的组件来实现)

买票服务器,在进行买票操作时,就需要先加锁

在redis上设置一个特殊的key-value,完成上述买票操作,再把这个key-value删除

其它服务器像买票时,也在redis尝试设置key-value,如果发现key-value已经存在,就认为"加锁失败"(是放弃/阻塞,看具体实现策略)

这样就可以保证,第一个服务器执行"查询->更新"过程中,第二个服务器不会出现"超卖"问题

setnx命令

回想setnx命令,使用setnx确实可以得到"加锁"效果,针对解锁,使用del命令

假设个服务器,加锁成功了(setnx成功),执行后续逻辑中程序崩溃了,没有执行解锁

那能否使用finally呢?(无论是否出现异常,都会执行到)

不能,这种做法,只是针对进程内(进程异常退出,锁也就随之销毁了)的锁,针对分布式锁无效(redis还是好好的)

这种情况就会导致redis上设置的key无人删除,其它服务器无法获取到锁了

解决:

可以给set的key设置过期时间,一旦时间到,自动删除

引入过期时间

set ex nx 命令

比如说,设置key的过期时间为1000ms,意味着即时出现极端情况,某个服务器挂了,没有正确释放锁,这个锁最多保持1000ms,也就会自动释放

注意:

setnx expire这种设置方式不可以

redis上的多个命令之间,无法保证原子性,此时就可能出现,这两个命令,一个成功,一个失败

相比之下,一条命令更稳妥

引入校验id

所谓的加锁,就是给redis设置一个key-value,解锁,就是给redis这个key-value删除掉

那么是否可能会出现,服务器1执行了加锁,服务器2执行了解锁

正常来说,是不会的,但是代码总会有bug,不小心就执行了解锁,此时就会进一步给系统带来严重的后果

为了解决上述问题,引入校验机制:

①给服务器编号,每个服务器有自己的身份标识

②进行加锁的时候,设置key-value,key对应着要针对哪个资源加锁,value存储服务器的编号,标识着当前这个锁是哪个服务器加上的

后续在解锁的时候,就可以进行校验了

解锁的时候,先查询一下这个锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果不是,就失败

当然,在解锁的时候,先查询判定,再进行del,此处是两步操作,就可能会出现问题

如果说:

一个服务器内部,也可能是多线程的

此时,就可能同一个服务器,两个线程都在执行上述解锁操作

看起来好像重复DEL好像问题不大,不是!!!

如果再引入一个新的服务器2,执行加锁操作,就可能出现问题了(在线程A执行完DEL之后,B执行DEL前)

服务器2的线程C执行加锁,此时由于A已经把锁释放的,C的加锁是能够成功的

但是紧接着,线程B的del到来了,就把刚刚服务器2的加锁操作给解锁了

服务器1和2进行加锁,key是资源的编号,服务器id是value

这个归根结底,都是因为get和del不是原子产生的问题

引入lua

使用事务,能解决上述问题(redis事务避免插队)

但是实际中往往采用更好的方案,lua脚本

lua脚本是一个编程语言,作为redis内嵌的脚本

lua语言特别轻量,实现一个lua解释器,销毁的体积非常小

可以使用lua编写一些逻辑,把这个脚本上传到redis服务器,就可以让客户端来控制redis执行上述脚本了

redis执行lua脚本的过程,也是原子的,相当于执行一条命令(实际上多条)

redis官方文档,也明确了lua是事务代替方案

引入watch dog(看门狗)

过期时间的续约问题:

在加锁的时候,给key设置过期时间

设置多少时间呢?

  • 设置过短,就可能业务逻辑还没有执行完,就释放锁了
  • 设置过长,就会导致"锁释放不及时"问题

更好的方式,就是"动态续约"

比如,初始情况下,设置一个过期时间(比如设置1s),提前在还剩300ms的时候,如果当前任务还没执行完,就把过期时间再续上1s,等到时间又快到了,但是任务还没执行完,就再次续约...

如果服务器中途崩溃了,就没人续约了,此时,锁就在较短的时间内被自动释放

"动态续约",续约服务器这边有一个专门的线程,负责续约这个事情=>看门狗

引⼊Redlock算法

那么使用redis作为分布式锁,redis本身有没有可能挂了呢?

当然有

要想保证"高可用",就需要通过这样一系列的"预案演习"

哨兵方式:

进行加锁,就是把key设置到主节点上

如果主节点挂了,有哨兵自动吧从节点升级为主节点,进一步保证刚才的锁仍然可用

主节点和从节点之间的数据同步,是存在延时的!!

可能主节点收到了set请求,还没来得及同步给从节点,主节点就先挂了

即使从节点升级成了主节点,但是刚才的加锁对应的数据也是不存在的

分布式系统,需要随时考虑某个节点挂了的情况,需要保证某个节点挂不会影响到大局


为了解决这个问题,Redis的作者提出了Redlock算法

引⼊⼀组Redis节点.其中每⼀组Redis节点都包含⼀个主节点和若⼲从节点.并且组和组之间存储的数据都是⼀致的,相互之间是"备份"关系

此处加锁,是按照一定的顺序,针对这些组redis都进行加锁操作

如果某个节点挂了(加不上锁),继续给下一个节点加锁即可

如果写入key成功的节点个数超过总数的一半,视为加锁成功

同理,解锁的时候,也就会把上述节点都设置一遍解锁

总之,Redlock算法的核⼼就是,加锁操作不能只写给⼀个Redis节点,⽽要写个多个!!

分布式系统 中任何⼀个节点都是不可靠的.最终的加锁成功结论是"少数服从多数的".

由于⼀个分布式系统不⾄于⼤部分节点都同时出现故障,因此这样的可靠性要⽐单个节点来说靠谱不 少

上述内容,只是一个简单的"互斥锁"

还涉及其它情况:

读写锁,公平锁(遵守先来后到),可重入锁...

相关推荐
paopaokaka_luck几秒前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法
python资深爱好者6 分钟前
NoSQL数据库与关系型数据库的主要区别
数据库·oracle·nosql
Hsu_kk8 分钟前
Kafka 安装教程
大数据·分布式·kafka
苍老流年10 分钟前
1. kafka分布式环境搭建
分布式·kafka
sj116373940311 分钟前
Kafka参数了解
数据库·分布式·kafka
Hsu_kk13 分钟前
Kafka Eagle 安装教程
分布式·kafka
CodingBrother16 分钟前
Kafka 与 RabbitMQ 的联系
分布式·kafka·rabbitmq
pblh12329 分钟前
2023_Spark_实验十五:SparkSQL进阶操作
大数据·分布式·spark
李少兄1 小时前
解决Spring Boot整合Redis时的连接问题
spring boot·redis·后端
日里安1 小时前
8. 基于 Redis 实现限流
数据库·redis·缓存