分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
本篇内容包括:关于 Redis 与 分布式锁,Redis 分布式锁的问题及解决方式,Redis 中的 Lua 脚本 以及 Redis 中的 RedLock 算法!
文章目录
- 一、关于 Redis 与 分布式锁
- 1、关于分布式锁
- 2、关于 Redis 实现分布式锁
- 二、Redis 分布式锁的问题及解决方式
- 三、Redis 中的 Lua 脚本
- 四、Redis 中的 RedLock 算法
- 1、Redis 中的 RedLock 算法
- 2、 Redlock 算法的客户端的执行步骤
一、关于 Redis 与 分布式锁
1、关于分布式锁
在一个分布式系统中,当一个线程去读取数据并修改的时候,因为读取和更新保存不是一个原子操作,在并发时就很容易遇到并发问题,进而导致数据的不正确。这种场景很常见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。
一般来说,实现分布式锁的方式有以下几种:
- 使用 MySQL,基于唯一索引。
- 使用 ZooKeeper,基于临时有序节点。
- 使用 Redis,基于 setnx 命令。
2、关于 Redis 实现分布式锁
Redis 实现分布式锁主要利用 Redis 的setnx
命令。setnx 是 SET if not exists(如果不存在,则 SET)的简写。
加锁:使用setnx key value
命令,如果 key 不存在,设置 value(加锁成功)。如果已经存在 lock(也就是有客户端持有锁了),则设置失败(加锁失败)
解锁:使用 del
命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过 setnx
命令进行加锁。
Key 的值可以根据业务设置,比如是用户中心使用的,可以命令为USER_REDIS_LOCK
,value 可以使用 uuid 保证唯一,用于标识加锁的客户端。保证加锁和解锁都是同一个客户端。
二、Redis 分布式锁的问题及解决方式
首先,有一个致命问题,就是某个线程在获取锁之后由于某些异常因素(比如宕机)而不能正常的执行解锁操作,那么这个锁就永远释放不掉了。为此,我们可以为这个锁加上一个超时时间为此,我们可以为这个锁加上一个超时时间
- 执行
SET key value EX seconds
的效果等同于执行SETEX key seconds value
- 执行
SET key value PX milliseconds
的效果等同于执行PSETEX key milliseconds value
然后,此时依然会有问题,某线程 A 获取了锁并且设置了过期时间为 10s,然后在执行业务逻辑的时候耗费了 15s,此时线程 A 获取的锁早已被 Redis 的过期机制自动释放了在线程A获取锁并经过 10s 之后,改锁可能已经被其它线程获取到了。当线程 A 执行完业务逻辑准备解锁(DEL key
)的时候,有可能删除掉的是其它线程已经获取到的锁。当解锁时,也就是删除 key 的时候先判断一下 key 对应的 value 是否等于先前设置的值,如果相等才能删除 key。
最后,这里我们还是一眼就可以看出问题来:GET
和DEL
是两个分开的操作,在 GET 执行之后且在 DEL 执行之前的间隙是可能会发生异常的,我们引入了一种新的方式,就是 Lua 脚本,解决原子性的问题。Redis 会将整个 Lua 脚本作为一个整体执行,中间不会被其他请求插入。
另外,为了防止多个线程同时执行业务代码,需要确保过期时间大于业务执行时间,可以在代码增加一个线程用于刷新定时过期时间,并增加一个 bool 类型的值表示是否开启定时刷新过期时间,在线程获取锁的时候,将其设置为 true,解锁前设置回 false。比如,Redisson 实现,获取锁成功就会开启一个定时任务,定时任务会定期检查去续期。
此外,还有一个问题:在集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。此处可以用 RedLock 算法解决。
三、Redis 中的 Lua 脚本
Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能
Redis 在 2.6 版本推出了 lua 脚本功能,允许开发者使用 Lua 语言编写脚本传到 Redis 中执行。
使用 Lua 脚本的好处:
- 原子操作。Redis 会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务;
- 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延;
- 复用。客户端发送的脚本会永久存在 Redis 中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。
四、Redis 中的 RedLock 算法
1、Redis 中的 RedLock 算法
在集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生
Redlock 算法就是为了解决这个问题
使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,Redlock 也使用大多数机制
加锁时,它会向过半节点发送 set 指令,只要过半节点 set
成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock
需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
2、 Redlock 算法的客户端的执行步骤
当 Redis 集群有 5 个节点,运行 Redlock 算法的客户端的执行步骤:
- 客户端记录当前系统时间,以毫秒为单位;
- 依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁,当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁的失效时间,避免因为网络故障出现的问题;
- 客户端使用当前时间减去开始获取锁时间就得到了获取锁使用的时间,当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功;
- 如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率;
- 如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁,即使是上一步操作请求失败的节点,防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致。