分布式锁原理有哪些,如何搞懂各种分布式锁?
摘要:分布式锁是控制分布式系统中不同进程节点对共享资源进行互斥访问的核心手段,其核心目标是保证在分布式环境下,同一时刻只有一个节点能获取到锁并执行临界区代码,避免数据不一致、并发冲突等问题。 本文将从分布式锁的核心要求出发,逐一拆解Redis、
分布式锁是控制分布式系统中不同进程/节点对共享资源进行互斥访问的核心手段,其核心目标是保证在分布式环境下,同一时刻只有一个节点能获取到锁并执行临界区代码,避免数据不一致、并发冲突等问题。
本文将从分布式锁的核心要求出发,逐一拆解Redis、ZooKeeper、Etcd、数据库等主流分布式锁的实现原理、优缺点及适用场景,帮你全面掌握其核心逻辑。
一、分布式锁的核心要求
要实现一个可靠的分布式锁,必须满足以下4个核心条件,缺一不可:
互斥性:同一时刻,只有一个客户端能持有锁。
防死锁:锁必须有超时自动释放机制,避免客户端宕机后锁永久持有。
可重入性(可选):同一客户端获取锁后,可重复获取同一把锁,避免死锁。
高可用/高性能:锁的获取/释放过程要高效,且能应对节点宕机等故障。
此外,还需满足原子性(获取/释放锁的操作是原子的)、容错性(集群环境下不丢锁)等附加要求。
二、基于Redis的分布式锁
Redis是分布式锁最常用的实现方案,基于其单线程执行和丰富的数据结构,实现简单且性能高。
1. 基础版Redis分布式锁(SETNX + EXPIRE)
实现原理
利用Redis的SETNX(SET if Not eXists,不存在则设置)命令实现锁的获取,EXPIRE设置锁超时时间防死锁:
获取锁:执行SETNX lock_key client_id,若返回1则获取锁成功;返回0则锁已被持有,获取失败。
释放锁:执行DEL lock_key,删除锁键即释放。
防死锁:通过EXPIRE lock_key 10设置锁超时(如10秒),即使客户端宕机,锁也会自动释放。
核心缺陷
原子性问题:SETNX和EXPIRE是两个独立命令,若执行完SETNX后客户端宕机,EXPIRE未执行,锁会永久持有。
锁误释放:客户端A获取锁后超时,锁自动释放;客户端B获取锁,此时A恢复并执行DEL删除B的锁,导致锁被误删。
不可重入:同一客户端重复执行SETNX会返回0,无法获取锁。
2. 进阶版Redis分布式锁(SET命令原子化)
实现原理
Redis 2.6.12+版本支持SET命令的扩展参数,可将获取锁+设置超时合并为一个原子操作,解决基础版的原子性问题:
获取锁:执行SET lock_key client_id EX 10 NX,其中:
EX 10:设置超时时间10秒;
NX:仅当键不存在时设置,等价于SETNX。
释放锁:需先判断锁的持有者(client_id)是否为当前客户端,再执行DEL,避免误释放。
示例:GET lock_key获取值,若等于当前client_id则DEL。
解决的问题
解决了SETNX+EXPIRE的原子性问题,避免锁永久持有。
增加client_id校验,避免误释放其他客户端的锁。
3. 高级版Redis分布式锁(Redlock算法)
实现原理
针对Redis单节点故障导致的锁丢失问题,Redis官方提出Redlock算法,基于Redis集群(多主节点)实现高可用分布式锁:
客户端获取当前时间戳(毫秒)。
依次向N个独立的Redis主节点(通常N≥5)执行获取锁操作(同进阶版SET lock_key client_id EX 10 NX),每个节点设置超时时间(远小于锁总超时,如50ms)。
客户端统计成功获取锁的节点数,若成功数≥N/2+1且总耗时<锁超时时间,则认为获取锁成功;否则释放所有节点的锁。
释放锁:向所有Redis节点执行释放锁操作(删除键)。
核心优势
解决单节点Redis宕机导致的锁丢失问题,高可用更强。
缺陷
性能损耗:需操作多个Redis节点,开销高于单节点锁。
复杂度高:算法实现繁琐,需处理节点宕机、时钟偏移等问题。
Redis分布式锁总结
方案
优点
缺点
适用场景
基础版(SETNX+EXPIRE)
实现简单
原子性差、易误释放、不可重入
低并发、简单场景
进阶版(SET EX NX)
原子性好、防误释放
单节点故障丢锁、不可重入
高并发、单Redis节点场景
Redlock算法
高可用、防丢锁
性能低、实现复杂
强一致性、高可靠场景
三、基于ZooKeeper的分布式锁
ZooKeeper是分布式协调框架,基于临时有序节点和Watcher监听机制实现分布式锁,天然支持高可用和防死锁。
1. 实现原理
ZooKeeper的数据模型是树形节点(ZNode),核心利用两类节点特性:
临时节点(EPHEMERAL):客户端宕机后,临时节点会自动删除,避免死锁。
有序节点(SEQUENTIAL):创建节点时自动分配递增序号,保证顺序。
独占锁(互斥锁)实现步骤
客户端在锁的根节点(如/lock)下创建临时有序节点,如/lock/lock-000001。
客户端获取/lock下的所有子节点,判断自己创建的节点是否为序号最小的节点:
若是:获取锁成功,执行临界区代码。
若否:监听比自己序号小1的节点(如创建lock-000002则监听lock-000001)的删除事件。
当监听到前一个节点删除时,重新判断自己是否为最小节点,若是则获取锁成功。
释放锁:客户端执行完临界区代码后,主动删除自己创建的临时节点。
可重入锁实现
在客户端本地记录锁的持有次数,获取锁时次数+1,释放时次数-1,次数为0时才删除节点。
2. 核心优势
高可用:ZooKeeper集群部署,节点宕机不影响锁服务。
防死锁:临时节点自动释放,避免锁永久持有。
公平性:基于有序节点,先请求先获取锁,保证公平性。
3. 缺陷
性能低于Redis:每次锁竞争需创建/删除节点,且Watcher监听有开销。
实现复杂度高:需处理节点监听、异常恢复等逻辑。
ZooKeeper分布式锁总结
优点
缺点
适用场景
高可用、公平性、防死锁
性能一般、实现复杂
强一致性、低并发高可靠场景
四、基于Etcd的分布式锁
Etcd是云原生场景下的分布式键值存储,基于Raft协议保证一致性,实现分布式锁的逻辑与ZooKeeper类似,但更轻量。
1. 实现原理
Etcd的核心特性:临时租约(Lease)、原子化CAS(Compare-And-Swap)、前缀监听。
独占锁实现步骤
客户端创建一个临时租约(设置超时时间,防死锁),并将租约与锁键(如/lock/resource)绑定。
执行原子化CAS操作:尝试将锁键的值设置为当前客户端ID,仅当键不存在时设置成功(获取锁)。
若CAS失败(锁已被持有),客户端监听锁键的删除事件,等待锁释放后重新竞争。
释放锁:客户端主动删除锁键,或租约到期后锁键自动删除。
2. 核心优势
基于Raft协议,强一致性,锁可靠性高。
轻量级,部署简单,适合云原生场景。
3. 缺陷
性能低于Redis,高于ZooKeeper。
生态适配性不如Redis广泛。
Etcd分布式锁总结
优点
缺点
适用场景
强一致、轻量、防死锁
性能一般、生态有限
云原生、强一致分布式场景
五、基于数据库的分布式锁
基于数据库的分布式锁是最原始的实现方案,利用数据库的行锁/表锁或唯一索引实现互斥。
1. 基于数据库表的悲观锁
实现原理
创建一张锁表,包含lock_key(锁名)、owner(持有者)、expire_time(过期时间)等字段:
获取锁:执行INSERT INTO lock (lock_key, owner, expire_time) VALUES ('resource1', 'client1', NOW()+10),利用数据库唯一索引的互斥性,插入成功则获取锁,失败则锁被持有。
释放锁:执行DELETE FROM lock WHERE lock_key='resource1' AND owner='client1'。
防死锁:通过expire_time定期清理过期锁。
2. 基于数据库表的乐观锁
实现原理
在业务表中增加version版本字段,通过CAS更新实现锁:
获取锁:无显式加锁,执行更新时判断版本:UPDATE table SET version=version+1 WHERE id=1 AND version=1,更新成功则获取锁(执行临界区),失败则重试。
释放锁:无显式释放,更新完成即释放。
数据库分布式锁总结
方案
优点
缺点
适用场景
悲观锁
实现简单
性能差、依赖数据库、易数据库单点故障
低并发、数据库已存在的场景
乐观锁
无锁竞争开销
仅适用于更新场景、需处理版本冲突
读多写少、低并发场景
六、各类分布式锁对比总结
锁方案
性能
可靠性
实现复杂度
公平性
适用场景
Redis(单节点)
极高
单节点故障丢锁
低
非公平
高并发、简单互斥场景
Redis(Redlock)
高
高可用
中
非公平
强一致、高可靠分布式场景
ZooKeeper
中
高可用
中高
公平
强一致、低并发高可靠场景
Etcd
中高
强一致
中
非公平
云原生、强一致分布式场景
数据库
低
依赖数据库
低
非公平
低并发、数据库已存在的场景
七、选型建议
高并发、简单互斥:选Redis单节点锁(性能最优)。
强一致、高可靠:选Redlock(Redis集群)、ZooKeeper或Etcd。
云原生场景:选Etcd。
低并发、依赖数据库:选数据库锁。
需要公平锁:选ZooKeeper。
核心原则:优先选Redis(性能),其次选ZooKeeper/Etcd(可靠),最后选数据库(依赖现有资源)。
