Redis 分布式锁:线程安全在分布式环境中的解决方案
Hello , 大家好 , 这个专栏给大家带来的是 Redis 系列 ! 本篇文章给大家讲解的是 Redis 的分布式锁. 我们会深入探讨分布式锁的概念和实现策略,这是分布式系统中确保多个进程间操作顺序和线程安全的重要机制。由于传统的锁机制仅在单个进程内部有效,分布式锁应运而生,以解决跨进程和跨主机的同步问题。
本专栏旨在为初学者提供一个全面的 Redis 学习路径,从基础概念到实际应用,帮助读者快速掌握 Redis 的使用和管理技巧。通过本专栏的学习,能够构建坚实的 Redis 知识基础,并能够在实际学习以及工作中灵活运用 Redis 解决问题 .
专栏地址 : Redis 入门实践
一 . 分布式锁
我们之前在多线程阶段学习到了一个问题 : 线程安全问题 , 通过线程安全问题引入了 “锁” 的概念 .
但是我们之前学习过的锁 , 本质上是只能在一个进程内部生效 , 而我们分布式系统中 , 是存在很多进程的 , 并且很多进程都是跨主机的 . 所以我们之前的锁就很难在分布式系统中发挥作用 , 并且在分布式系统中多个进程之间的执行顺序也是不确定的 , 更何必线程了 .
所以我们就需要引入一个新的 “分布式锁” 来解决上述问题
比如我们现在有这样的一个场景 : 买票
二 . 分布式锁的实现策略
2.1 引入 setnx
我们刚才介绍分布式锁的时候 , 触发查询操作的时候 , 就需要先在 Redis 中设置一个键值对 , 表示我现在正在查询 , 如果其他客户端查询这个键值对不为空 , 他就知道了我需要阻塞等待 .
那这个键值对实际上就是不存在才去设置 , 如果存在就不设置 , 那这不就是我们的 setnx 操作吗 .
虽然使用 setnx 可以得到分布式加锁效果 , 但是加锁之后我们还需要解锁 , 就可以使用 del 命令来去解锁 , 来去删除掉 Redis 中的键值对 .
但是我们考虑一个极端的情况 : 某个服务器加锁成功了 (setnx 执行成功) , 但是在执行后续逻辑过程中 , 程序崩溃了 (也就是没有执行到解锁操作 , 也就是 del 命令) , 这就比较尴尬了 .
2.2 引入过期时间
我们可以给设置的 key 设置过期时间 , 一旦时间到了 , key 就会自动被删除掉了 .
我们可以使用 set ex nx 这样的命令来去设置 .
但是使用 setnx + expire 这两条命令是不行的 , 因为 Redis 的多个命令是无法保证原子性的 , 就有可能出现一个成功一个失败的情况 . 相比之下 , 使用一条命令设置更加稳妥 .
这样的话 , 如果出现极端情况导致某个服务器挂了 , 没有正确的释放锁 , 那这个锁到达过期时间也就会自动释放了 .
2.3 引入校验 ID
我们介绍的 , 所谓分布式锁
- 加锁就是给 Redis 上设置一个 key-value
- 解锁就是将 Redis 上的 key-value 删除掉
那就有可能出现服务器 1 执行了加锁 , 服务器 2 执行了解锁 , 这样就出现一些不可预料的错误 , 导致分布式锁形同虚设 , 从而进一步引起超卖问题 .
那为了解决这个问题 . 就需要引入一些校验机制
- 给服务器编号 , 每一个服务器都有自己的标识
- 进行加锁的时候 , 设置 key-value , key 对应着要针对哪个资源进行加锁 , value 就可以存储刚才服务器的编号 , 标识出当前这个锁是哪个服务器加上的
- 后续解锁的时候 , 就可以针对 value 进行校验了 . 在解锁的时候先查询一下这个锁对应的服务器编号 (get key 操作) , 然后判定一下这个编号是否就是当前执行解锁操作的服务器编号 . 如果是才能执行 del , 如果不是就执行失败 .
那这些逻辑 , 就需要服务器端来去完成的逻辑 , 通过上述校验规则 , 就可以有效避免 “锁误删” 的概念 .
2.4 引入 lua 脚本
我们在解锁操作的时候 , 需要先查询判定 , 再进行 del 操作 . 那这两个操作不是原子的 , 就也有可能出现问题 .
比如 : 一个服务器内部 , 也有可能是多线程的 . 此时就有可能一个服务器中两个线程都在执行上述解锁操作
我们可以使用 Lua 语言来去编写一些逻辑 , 然后把这个脚本上传到 Redis 服务器上 , 然后就可以让客户端来控制 Redis 执行上述脚本了 . 并且 Redis 执行 Lua 脚本的过程 , 也是原子的 , 实际上就相当于执行一条命令 .
我们可以给一个示例代码来看一下 :
-- ARGV 是调用其他的脚本,需要给定参数,此处就需要传入一个服务器的 ID
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1]) -- ID 匹配就执行删除操作
else
return 0
end;
2.5 引入看门狗
那新的问题也就出现了 , 我们在加锁的时候需要设置过期时间 . 那这个过期时间设置成多少合适 ?
- 如果设置的太短 , 就有可能在业务逻辑执行完毕之前 , 锁就被释放了
- 如果设置的太长 , 就也会导致锁释放的不及时的问题
相比之下 , 我们更倾向于 “动态续约” 这种方式 .
在初始情况下 , 设置一个过期时间 (比如 : 设置 1s) , 就提前在还剩 300ms 的时候 (数值可以灵活调整 , 只是举个例子) , 判断当前任务是否执行完 . 如果当前任务还没执行完 , 就把过期时间再续上 1s . 等到时间又快到了 , 任务还没执行完 , 那就继续续约 .
如果服务器中途崩溃了 , 自然就没人负责续约了 , 此时锁就能够在较短时间内被自动释放 .
2.6 引入 redlock 算法
我们使用 Redis 来作为分布式锁 , 那 Redis 本身有没有可能挂了呢 ?
当然有可能 , 要想保证高可用 , 我们就需要通过一系列的 “预案演戏” .
我们之前也学习过各种场景
- 主从复制
- 哨兵 : 保证高可用的最佳方案
- 集群 : 更多的是解决存储空间不足的问题
那如何使用哨兵保证高可用的呢 ?
我们进行加锁操作 (set nx ex) , 就是把 key 写入到主节点中 , 然后同步给从节点 . 哨兵节点负责监控每个节点的工作状态 , 如果主节点挂了 , 就有哨兵自动的把从节点升级成主节点 , 进一步保证刚才的加锁操作依然有效 .
但是主节点和从节点之间同步数据是存在延时的 . 可能主节点收到了 set 请求 , 还没来得及同步给从节点 , 主节点就先挂了 . 即使从节点升级成了主节点 , 但是刚才加锁操作也是丢失的了 .
作为分布式系统 , 就需要随时考虑某个节点挂了的情况 , 需要保证某个节点挂了不会影响到大局
Redis 作者给出的方案是 redlock 算法 , 实际上就是一个冗余的思路
那此时 , 如果进行加锁操作 , 就会按照一定的顺序针对这些组的 Redis 都进行加锁操作 .
如果某个节点挂了 , 那就继续给下一个节点加锁即可 .
如果当前加锁成功的个数大于当前总节点总数的一半 , 我们就认为加锁成功 . 此时就不会因为某个 Redis 节点挂了导致影响整体操作 .
同时 , 解锁的时候也会把所有节点都设置一遍解锁操作 .
到此为止 , 我们分布式锁的内容也介绍完毕了 , 整个 Redis 模块我们也就介绍完毕了 , 希望大家能够通过我的文章了解并学习到 Redis 相关的知识 .
如果对你有帮助的话 , 还请一键三连~ 咱们下个专题见