【Redis应用】-- 分布式锁
文章目录
1. Redis应用–分布式锁
1.1 什么是分布式锁
在一个分布式系统中,也会涉及到多个节点访问统一公共资源的情况。此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”的问题。
而 Java 的 synchronized 或者 C++ 的 std::mutex,这样的锁都是只能在当前进程中生效,在分布式锁的这种多个进程多个主机之间就很难产生制约,分布式系统中多个进程之间的执行顺序也是不确定的。此时就需要分布式锁。
1.2 分布式锁的实现
分布式锁也是一个/一组单独是服务器程序,给其他的服务器提供加锁服务。
Redis就是一种典型的可以用来实现分布式锁的方案。

以买票服务器为例:
1. 在进行买票操作的时候,先加锁(在Redis 上设置一个特殊的 key-value,完成了买票操作,再把这个 key-value 删除掉。)
2. 其他服务器也想买票的时候,也去 Redis 上尝试设置 key-value,如果发现 key-value 已经存在,就认为是加锁失败。
3. 这样就保证了在第一个服务器执行“查询-更新"过程中,第二个服务器不会执行查询,也就解决了超卖问题。
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 还没有成功,就视为加锁失败。
如果给某个节点加锁失败,就⽴即再尝试下⼀个节点。 当加锁成功的节点数超过总节点数的⼀半,才视为加锁成功。这样的话,即使有某些节点挂了,也不影响锁的正确性。
同理,释放锁的时候,也需要把所有节点都进⾏解锁操作(即使是之前超时的节点, 也要尝试解锁,尽量保证逻辑严密)。
更多推荐



所有评论(0)