【Redis】-- 分布式锁
文章目录
- 1. Redis应用--分布式锁
- 1.1 什么是分布式锁
- 1.2 分布式锁的实现
- 1.3 引入过期时间
- 1.4 引入校验id
- 1.5 引入lua
- 1.6 引入watch dog(看门狗)
- 1.7 引入Redlock算法
1. Redis应用–分布式锁
1.1 什么是分布式锁
在一个分布式系统中,也会涉及到多个节点访问统一公共资源的情况。此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”的问题。
而Java的synchronized或者C++的std::mutex,这样的锁都是只能在当前进程中生效,在分布式锁的这种多个进程多个主机之间就很难产生制约,分布式系统中多个进程之间的执行顺序也是不确定的。此时就需要分布式锁。
1.2 分布式锁的实现
分布式锁也是一个/一组单独是服务器程序,给其他的服务器提供加锁这样的服务。
Redis就是一种典型的可以用来实现分布式锁的方案。
以买票服务器为例:
- 在进行买票操作的时候,先加锁(在Redis上设置一个特殊的key-value,完成了买票操作,再把这个key-value删除掉。)
- 其他服务器也想买票的时候,也去Redis上尝试设置key-value,如果发现key-value已经存在,就认为加锁失败。
- 这样就保证了再第一个服务器执行“查询-更新"过程中,第二个服务器不会执行查询,也就解决了超卖问题。
1.3 引入过期时间
使用set nx可以达到加锁的效果;使用del来完成解锁的效果。
但是如果设置的key-value还没来得及解锁,进程异常终止了,这样就会导致Redis上设置的key无人删除,也就导致其他服务器无法获取到锁了。
这种情况就可以给set的key设置一个过期时间,一旦时间到了,key就会被自动删除掉。
不能使用setnx expire这两个命令分开设置,务必使用set ex nx这样的方式来进行设置。因为Redis上的多个命令之间是无法保证原子性的,此时就可能出现一个命令成功,一个失败的情况。相比之下,使用一条命令设置更加稳妥。
1.4 引入校验id
那么是否会出现服务器1执行了加锁,服务器2执行了解锁操作呢?
正常来说一般肯定不是故意的,但是代码总会有bug,不小心执行到了解锁操作,因此就可能进一步的给系统带来更严重的问题。
为了解决上述问题,就需要引入一点校验机制。
- 给服务器进行编号,每个服务器都有一个自己的身份标识。
- 进行加锁是的时候,设置key-value,key对应着要针对哪个资源加锁,value就可以存储刚才服务器的编号,标识出当前这个锁是哪个服务器加上的。
- 后续再解锁的时候,先查询一下锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果是,才能真正执行del;如果不是,就解锁失败。
1.5 引入lua
在通过上述的校验的方式来进行解锁:
1. 查询判定。
2. del
此处的操作是两步操作,不是原子的,就可能会出现问题。
前提:服务器1执行了加锁操作。
- 服务器1的线程A和线程B先后执行了GET操作进行校验,由于是服务器1进行的加锁操作,所以线程A和线程B都能校验通过。
- 服务器1的线程A先执行了DEL操作,实现了解锁。
- 在服务器1的线程A执行DEL和线程B执行DEL之间,服务器2的线程C执行来了set nx ex加锁操作,此时肯定是能加锁成功的。
- 然后服务器1的线程B执行了DEL操作。
此时就出现了问题。
虽然Redis中的事务能够解决上面的问题,但是在实践中往往是使用lua脚本。
lua是一个编程语言,作为Redis内嵌的脚本。MySQL8支持js作为内嵌语言。lua语言特别轻量(实现一个lua解释器,消耗的体积是非常小的)。
可以使用lua编写一些逻辑,把这个lua脚本上传到Redis服务器上,然后可以让客户端来控制Redis执行上述脚本。
Redis执行lua脚本的过程也是原子的,相当于执行一条命令。
1.6 引入watch dog(看门狗)
我们要在加锁的时候给key设置一个过期时间。
- 如果过期时间设置的短,就可能在业务逻辑还没有执行完就释放锁了。
- 如果过期时间设置的过长,就会导致锁释放不及时的问题。
那么最好的方式就是动态续约,往往需要服务器这边一个专门的线程,负责续约的事情,我们把这个负责的线程,叫做“看门狗”。
初始情况下,设置一个过期时间(比如说是1s)就提前在还剩300ms(可灵活调整)的时候,如果当前任务还没有执行完,就把过期时间再续上1s,等到时间又快到了,任务还没执行完就再续上。
1.7 引入Redlock算法
使用Redis作为分布式锁,redis是有可能挂的。那么我们就要保证高可用。
在集群中,主节点和从节点之间的数据同步,是存在延迟的。可能主节点收到了set请求,但是还没来得及同步给从节点的时候主节点就已经挂了,即使从节点升级成了主节点,但是刚才的set的key-value也已经不存在了。
Redis作者针对这个问题给出了一个方案。那就是redback算法(冗余)。
我们引⼊⼀组 Redis 节点. 其中每⼀组 Redis 节点都包含⼀个主节点和若⼲从节点. 并且组和组之间存储的数据都是⼀致的, 相互之间是 “备份” 关系(⽽并⾮是数据集合的⼀部分, 这点有别于 Redis cluster). 加锁的时候, 按照⼀定的顺序, 写多个 master 节点. 在写锁的时候需要设定操作的 “超时时间”. ⽐如 50ms. 即如果 setnx 操作超过了 50ms 还没有成功, 就视为加锁失败.
如果给某个节点加锁失败, 就⽴即再尝试下⼀个节点. 当加锁成功的节点数超过总节点数的⼀半, 才视为加锁成功. 这样的话, 即使有某些节点挂了, 也不影响锁的正确性.
同理, 释放锁的时候, 也需要把所有节点都进⾏解锁操作. (即使是之前超时的节点, 也要尝试解锁, 尽量保证逻辑严密).