如何理解分布式锁
Redis 可以通过 set key 方式来实现分布式锁
实际开发中还要考虑如何确保超时情况下的串行,如何合理的释放锁等
本文要讲的是一个完备的分布式锁应该具备哪些特性
以及如何使用 Redis 来一步步优化实现
分布式锁需要具有哪些特点
- 互斥性
互斥是锁的基本特征,同一时刻只能有一个线程尺持有锁,执行临界操作 - 超时释放
超时释放是锁的另一个必备特性
可以对比 MySQL InnoDB 引擎中的 innodb_lock_wati_timeout 配置
通过超时释放,防止不必要的线程等待和资源浪费 - 可重入性
在分布式环境下,同一个节点上的同一个线程如果获取了锁之后,再次请求还是可以成功 - 高性能和高可用
加锁和解锁的开销要尽可能小,同时也要需要保证高可用,防止分布式锁失效 - 支持阻塞和非阻塞性
对比 Java 语言中的 wait() 和 notify() 等操作,这个一般是在业务代码中实现,比如在获取锁时通过 while(true) 或者轮询来实现阻塞操作
使用 setnx 实现分布式锁
使用 setnx 实现分布式锁的方案:以该锁为 key 设置一个随机的值
- 如果setnx 返回 1 ,则说明该进程获得锁
- 如果 setnx 返回 0,则说明其他进程已经获取了锁,进程不能进入临界区
如果需要阻塞当前进程,不断尝试 setnx 操作
if (setnx(key, value) == 1) {
try {
// 业务处理
} finally {}
// 释放锁
del(key)
}
}
使用 Java 中的 try-catch-finally 来完成锁的释放
使用这种方式来实现分布式锁的问题:不支持超时释放锁
如果进程在加锁后宕机,则会导致锁无法删除,其他进程无法获得锁
使用 setnx 和 expire 实现
if (setnx(key, value) == 1) {
expire(key, expireTime)
try {
// 业务处理
} finally {
// 释放锁
del(key)
}
}
Redis 在设置一个 key 时,支持设置过期时间
可以在缓存中实现锁的超时释放,解决死锁问题
在 Redis 中,setnx 和 expire 这两个命令不具备原子性
在 Redis 2.8 版本中,扩展了 set 命令
支持 set 和 expire 指令组合使用
解决了加锁过程中失败的问题
SET key value expireTime nx
nx 表示仅在键不存在时设置
可以在同一时间内完成设置值和设置过期时间这两个操作
防止设置过期时间异常导致的死锁
set 扩展命令还存在问题吗?
在加锁和释放锁之间的业务逻辑执行的太长
以至于超出了锁的超时限制
缓存将对应的 key 删除,其他线程可以获取锁
出现对加锁资源的并发操作
案例:
- 客户端 A 获取锁的时候设置了 key 的过期时间为 2 秒
客户端 A 在获取到锁之后,业务逻辑方法执行了 3 秒 - 客户端 A 获取的锁被 Redis 过期机制自动释放,客户端 B 请求锁成功,出现并发执行
- 客户端 A 执行完业务逻辑后,释放锁,删除对应的key
- 对应锁已经被客户端 B 获取到了,客户端 A 释放的锁实际是客户端 B 持有的锁
如何避免上述问题?
- 首先,基于 Redis 的分布式锁一般是用于耗时比较短的瞬时业务,业务上超时的可能性比较小
- 其次,在获取锁时,可以设置 value 为一个随机数,在释放锁时进行读取和对比
确保释放的当前线程持有的锁,一般是通过 Redis 结合 Lua 脚本的方案实现 - 最后,需要添加完备的日志,记录上下游数据链路
当出现超时,则需要检查对应的问题数据,并且进行人工修复
分布式锁的高可用
在生产环境中,为了保证高可用,避免单点故障,通常会使用 Redis 集群
集群下分布式锁存在哪些问题
集群环境下,Redis 通过主从复制来实现数据同步,Redis 的主从复制(Replication)是异步的
所以单节点下可用的方案在集群的环境中可能会出现问题
在故障转移(Failover)过程中丧失锁的安全性
假设 Master 节点获取到锁后在未完成数据同步的情况下,发生节点崩溃
此时在其他节点依然可以获取到锁,出现多个客户端同时获取到锁的情况
模拟这个场景,按照下面的顺序执行:
- 客户端 A 从 Master 节点获取锁
- Master 节点宕机,主从复制过程中,对应锁的 key 还没有同步到 Slave 节点上
- Slave 升级为 Master 节点,于是集群丢失了锁数据
- 其他客户端请求新的 Master 节点,获取到了对应同一个资源的锁
- 出现多个客户端同时持有同一个资源的锁,不满足锁的互斥性
关于集群下如何实现分布式锁 Redis 的作者提出了 Redlock 算法
Redlock 算法的流程
Redlock 算法是在单 Redis 节点基础上引入的高可用模式
Redlock 基于 N 个完全独立的 Redis 节点
一般是大于 3 的奇数个(通常情况下 N 可以设置为 5)
可以基于保证集群内各个节点不会同时宕机
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作:
- 客户端记录当前系统时间,以毫秒为单位
- 依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁
当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间
超时时间应该小于锁的失效时间,避免因为网络故障出现的问题 - 客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间
当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效的时间时,锁才算获取成功 - 如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率
- 如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁
防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致
在 Redis 官方推荐的 Java 客户端 Redisson 中,内置了对 RedLock 的实现
在实际业务中,一般使用基于单点的 Redis 实现分布式锁,出现数据不一致,通过人工手段去回补。