Redis ①⑦-分布式锁

分布式锁

分布式锁是锁的一种,都是为了解决多线程/多进程环境下,对共享资源的访问冲突问题。

不过,像 Java 的 synchronized 或者 C++ 的 mutex 这种锁,都是进程内的锁,而分布式锁则是跨越进程/机器的锁。也就是可以针对多个进程、多台服务器的的共享资源进行加锁

考虑买票的场景,现在车站提供了若干个车次,每个车次的票数都是固定的。

现在存在多个服务器节点,都可能需要处理这个买票的逻辑:先查询指定车次的余票,如果余票 > 0,则设置余票值 -= 1

上述场景如果任何处理,就会存在 "线程安全问题"。

当多个客户端同时购买一个车次的票时,此时余票数量还未作出相应的更新。

假设此时只剩一张余票,当多个客户端都判断 余票 > 0 成功后,导致余票 "被卖出去了多张",但实际只剩一张了,这就是是出现 "超卖" 的情况。

为了解决这个问题,引入分布式锁。

实现分布式锁

引入 SETNX

既然普通的锁无法跨越多个进程/机器,那就只能单独找一台服务器作为锁的管理者,该服务器可以使用 Redis 来实现分布式锁。

某个客户端执行买票请求前,先访问 Redis,在 Redis 上设置一个键值对。比如 key 就是车次,value 随便设置个值 (比如 1)。

当另一个客户端也想要访问买票请求时,也先访问 Redis,在 Redis 上设置一个 key 为该车次的键值对,如果发现 key 已经设置过了,说明有其他客户端正在处理该车次的票,则该客户端要么直接放弃,要么等待。

而当该客户端处理完买票请求后,直接删除该 key 即可。就相当于是解锁操作。

如何实现?可以使用 Redis 的 setnx 命令,该命令的作用是设置一个键值对,当且仅当键不存在时才设置成功。

引入过期时间

但是上述方案存在问题,如果某个客户端设置完锁 key 后,突然挂掉了,此时锁一直存在,其他客户端就无法获取到锁,导致其他客户端无法正常处理买票请求。

所以,我们可以给锁引入一个过期时间,比如 1 秒,即这个锁的最长持有时间,如果锁的过期时间到了,则自动释放锁。

如何实现?可以使用 Redis 的 set ex nx 命令,在设置锁的同时把过期时间加上。

能否先设置锁,然后通过 expire 的方式设置过期时间?答案不行的,因为这是两条命令了,就不是原子的了,就有概率发生 "线程安全问题"。

引入校验ID

上述依然存在问题,是否可能会出现 服务器1 设置锁,服务器2 误删锁。答案是可能的,保不齐代码哪里会出现 bug,导致锁被误删。

正常的解锁操作是必须由加锁的这一方执行的。

所以,我们可以给每个服务器设置一个唯一的 ID,在设置锁,将这个锁的 value 设置为这个 ID。

在解锁时,先获取到该锁,校验其 value 是否与当前服务器的 ID 相同,如果相同,则执行解锁操作。如果不相同,则忽略该操作。

引入 Lua 脚本

上述解锁的判断依然可能存在问题,根本原因还是可能造成 "线程安全问题"。

校验和删除这两个操作,不是原子的,则可能会出现下图的情况:

如果发生上述情况,命令按照上图的顺序执行,会导致 服务器1 删除锁之后,服务器3 来加锁了,但是马上又被 服务器2 给删了。

我们可以使用事务将这两个操作打包成一个原子的操作,但是,Redis 的事务比较鸡肋,形同虚设。

Redis 官方文档明确说,Lua 脚本可以作为事务的替代方案。Redis 在执行 Lua 脚本时,就相当于是执行一条命令,只有全部命令都执行完了,才会服务其他客户端。

使用 Lua 脚本完成上述功能:

lua 复制代码
if redis.call("setnx", KEYS[1]) == ARGV[1] then
    return redis.call("expire", KEYS[1])
else
    return 0
end

上述代码可以编写成一个 .lua 后缀的文件,由 redis-cli 或者 redis-plus-plus 或者 jedis 等客户端加载,并发送给 Redis 服务器,由 Redis 服务器来执行这段逻辑。

一个 lua 脚本会被 Redis 服务器以原子的方式来执行。

引入看门狗

上述的方案依然存在问题,就是锁的过期时间较为固定。

如果锁的过期时间设置的过短,可能业务逻辑还没处理完就释放锁了。

如果锁的过期时间设置的过长,导致锁的持有时间太长,导致其他客户端无法正常处理请求。

所以我们采用动态续约的机制,引入一个 "看门狗" 线程,每隔一段时间,如果当前业务还没执行完,就续上锁的过期时间。

引入 Redlock 算法

实践中的 Redis 一般是以集群的方式部署的(至少是主从的形式,而不是单机)。那么就可能出现以下比较极端的情况:

  • 服务器1master 节点进行加锁操作。这个写入 key 的过程刚刚完成,master 就挂了;slave 节点升级成了新的 master 节点。
  • 但是由于刚才写入的这个 key 尚未来得及同步给 slave,此时就相当于 服务器1 的加锁操作形同虚设了,服务器2 仍然可以进行加锁(即给新的 master 写入 key。因为新的 master 不包含刚才的 key)。

为了解决这个问题,Redis 官方提出了 Redlock 算法。

我们引入一组 Redis 节点。其中每一组 Redis 节点都包含一个主节点和若干从节点。并且组和组之间存储的数据都是一致的,相互之间是 "备份" 关系 (而并非是数据集合的一部分。这点有别于 Redis cluster)。

加锁的时候,按照一定的顺序,写多个 master 节点。在写锁的时候需要设定操作的 "超时时间"。比如 50ms。即如果 setnx 操作超过了 50ms 还没有成功,就视为加锁失败。

如果给某个节点加锁失败,就立即再尝试下一个节点。

当加锁成功的节点数超过总节点数的一半,才视为加锁成功。

这样的话,即使有某些节点挂了,也不影响锁的正确性。

同理,释放锁的时候,也需要把所有节点都进行解锁操作。(即使是之前超时的节点,也要尝试解锁,尽量保证逻辑严密)。

简而言之,Redlock 算法的核心就是,加锁操作不能只写给一个 Redis 节点,而要写给多个!!!

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

证逻辑严密)。

简而言之,Redlock 算法的核心就是,加锁操作不能只写给一个 Redis 节点,而要写给多个!!!

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

由于一个分布式系统不至于大部分节点都同时出现故障,因此这样的可靠性要比单个节点来说靠谱不少。

相关推荐
····懂···17 分钟前
关于PGCE专家技术认证解决方案
数据库·postgresql
失散1324 分钟前
大型微服务项目:听书——10 缓存+分布式锁优化根据专辑id查询专辑详情接口
redis·分布式·缓存·微服务
秋千码途27 分钟前
小架构step系列22:加载系统配置
数据库·架构
zone_z28 分钟前
告别静态文档!Oracle交互式技术架构图让数据库学习“活“起来
数据库·学习·oracle
旧时光巷1 小时前
SQL基础⑭ | 变量、流程控制与游标篇
数据库·sql·学习·mysql·变量·游标·流程控制
PythonicCC1 小时前
Django模板系统
数据库·django
小云数据库服务专线1 小时前
GaussDB view视图的用法
数据库
Aeside12 小时前
Redis的线程模型
redis
DBLens数据库管理和开发工具2 小时前
MySQL新增字段DDL:锁表全解析、避坑指南与实战案例
数据库·后端
white camel2 小时前
分布式方案 一 分布式锁的四大实现方式
redis·分布式·zookeeper·分布式锁