Tags: Redis, 分布式
Published: 2024年6月23日
一般来说,生产环境可用的分布式锁需要满足以下几点:
Redis 支持 setnx 指令,只在 key 不存在的情况下,将 key 的值设置为 value,若 key 已经存在,则 setnx 命令不做任何动作。如果 setnx 返回 1,则说明该进程获得锁;如果 setnx 返回 0,则说明其他进程已经获得了锁,进程不能进入临界区;如果需要阻塞当前进程,可以在一个循环中不断尝试 setnx 操作。
对比一下上面说的分布式锁特性,使用这种方式实现分布式锁的问题很明显:不支持超时释放锁,如果进程在加锁后宕机,则会导致锁无法删除,其他进程无法获得锁。
在分布式锁的实现中,依赖业务线程进行锁的释放,如果进程宕机,那么就会出现死锁。Redis 在设置一个 key 时,支持设置过期时间,利用这一点,可以在缓存中实现锁的超时释放,解决死锁问题。
在使用 setnx 获取锁之后,通过 expire 给锁加一个过期时间,利用 Redis 的缓存失效策略,进行锁的超时清除。
通过设置过期时间,避免了占锁到释放锁的过程发生异常而导致锁无法释放的问题,但是在 Redis 中,setnx 和 expire 这两条命令不具备原子性。如果一个线程在执行完 setnx 之后突然崩溃,导致锁没有设置过期时间,那么这个锁就会一直存在,无法被其他线程获取。
为了解决这个问题,在 Redis 2.8 版本中,扩展了 set 命令,支持 set 和 expire 指令组合的原子操作,解决了加锁过程中失败的问题。
abapredis> SET key value expireTime nx
nx 表示仅在键不存在时设置,这样可以在同一时间内完成设置值和设置过期时间这两个操作,防止设置过期时间异常导致的死锁。那么这种方式还存在问题吗?
使用 setex 方式看起来解决了锁超时的问题,但在实际业务中,如果对超时时间设置不合理,存在这样一种可能:在加锁和释放锁之间的业务逻辑执行的太长,以至于超出了锁的超时限制,缓存将对应 key 删除,其他线程可以获取锁,出现对加锁资源的并发操作。
如何避免这个问题呢?
上面分布式锁的实现方案中,都是针对单节点 Redis 而言的,在生产环境中,为了保证高可用,避免单点故障,通常会使用 Redis 集群。
集群环境下,Redis 通过主从复制来实现数据同步,Redis 的主从复制(Replication)是异步的,所以单节点下可用的方案在集群的环境中可能会出现问题,在故障转移(Failover) 过程中丧失锁的安全性。
由于 Redis 集群数据同步是异步的,假设 Master 节点获取到锁后在未完成数据同步的情况下,发生节点崩溃,此时在其他节点依然可以获取到锁,出现多个客户端同时获取到锁的情况。
我们模拟下这个场景,按照下面的顺序执行:
关于在集群中如何实现分布式锁,可以使用Redlock算法
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作:
分布式系统设计是实现复杂性和收益的平衡,考虑到集群环境下的一致性问题,也要避免过度设计。在实际业务中,一般使用基于单点的 Redis 实现分布式锁就可以,出现数据不一致,通过人工手段去回补。
本文作者:AstralDex
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!