分布式锁实现方式:从原理到选型,一篇讲透
面试官:“分布式锁怎么实现?”
你:“主要有三种方式:基于 Redis 的SET NX EX、基于 Zookeeper 的临时顺序节点、基于数据库的悲观锁或乐观锁。企业最常用的是 Redis 分布式锁,性能高且实现简单。”
面试官:“那 Redis 锁有什么坑?如何保证锁的原子性?如果持有锁的线程挂了怎么办?”
你:“……”
很多人能说出三种方式,但一追问 Redis 锁的原子性、锁续命、Zookeeper 的羊群效应、数据库锁的性能瓶颈就含糊了。本文从原理到实战,彻底讲透分布式锁的实现与选型。
一、为什么需要分布式锁?
在单机环境下,通过synchronized或ReentrantLock即可保证线程安全。但在分布式系统中,多个服务实例同时操作共享资源(如数据库、文件、缓存)时,就需要分布式锁来互斥访问。分布式锁应满足:
- 互斥性:任何时刻只有一个客户端持有锁。
- 容错性:锁服务高可用,能自动释放锁(防死锁)。
- 阻塞/非阻塞:通常支持尝试获取锁超时。
- 可重入性(可选):同一线程可重复获取锁。
二、基于 Redis 的分布式锁
1. 实现原理
Redis 实现分布式锁最常用的命令是SET key value NX EX seconds:
NX:只有 key 不存在时才设置成功(保证互斥)。EX:设置过期时间,防止死锁(客户端崩溃后自动释放)。
解锁时,需要先GET检查 value 是否为本客户端设置的随机值(防止误删其他客户端的锁),然后DEL。这一过程必须原子操作,通常使用 Lua 脚本。
2. 核心代码示例(Jedis)
// 加锁StringlockKey="order:1001";StringrequestId=UUID.randomUUID().toString();booleansuccess=jedis.set(lockKey,requestId,"NX","EX",30)!=null;// 解锁(Lua 脚本保证原子性)Stringscript="if redis.call('get', KEYS[1]) == ARGV[1] then "+"return redis.call('del', KEYS[1]) "+"else return 0 end";Objectresult=jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));3. 常见问题与解决方案
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 原子性问题 | 加锁需要NX+EX同时设置 | Redis 2.6.12+ 支持单条原子命令 |
| 误删锁 | 线程 A 的锁超时自动释放,线程 B 获得锁,A 执行完却删除了 B 的锁 | value 设为随机 ID,解锁时判断是否匹配(用 Lua) |
| 锁过期,任务未完成 | 执行耗时超过过期时间,锁自动释放 | 锁续命(watchdog):起一个守护线程,定期检查并续期 |
| 单点故障 | 主从架构下,主节点宕机,锁信息未同步到从节点 | Redlock算法(多独立 Redis 实例)或使用 Zookeeper |
4. Redlock 算法(Redisson 实现)
为了避免 Redis 单点问题,Redis 作者提出了 Redlock:客户端向多个独立的 Redis 节点(通常 5 个)请求锁,只有超过半数(N/2+1)节点加锁成功且总耗时小于锁过期时间,才认为获得锁。释放时需要向所有节点发送释放命令。大多数场景下,单 Redis 实例加上哨兵/集群已能满足,Redlock 过于复杂且有一定争议。
生产推荐:使用Redisson框架,它封装了看门狗(自动续期)和 Redlock,API 简单。
// Redisson 使用示例RLocklock=redissonClient.getLock("myLock");lock.lock(30,TimeUnit.SECONDS);// 自动续期(默认看门狗每 10 秒续期 30 秒)try{// ...}finally{lock.unlock();}三、基于 Zookeeper 的分布式锁
1. 实现原理
Zookeeper 的数据节点(ZNode)具有以下特性:
- 临时节点(Ephemeral):创建该节点的客户端会话断开后,节点自动删除。
- 顺序节点(Sequential):节点名后追加递增序号,如
lock-00000001。
分布式锁的实现步骤:
- 在锁目录下创建临时顺序节点(如
/locks/lock-00000001)。 - 获取
/locks下所有子节点,排序,若当前节点序号最小,则获得锁。 - 否则,监听前一个序号节点的删除事件(避免羊群效应),监听到后重新判断。
- 释放锁时,删除自己创建的临时节点(会话断开也会自动删除)。
2. 优点
- 强一致性:Zookeeper 基于 ZAB 协议,保证数据一致性。
- 没有锁过期问题:临时节点自动清理,避免了 Redis 锁过期任务未完成的尴尬(除非网络分区导致临时节点存活但业务线程已死,但概率极低)。
- 可避免羊群效应:只监听前一个节点,而非所有节点。
3. 缺点
- 性能较低:Zookeeper 的创建、删除、监听都有一定延迟(相比 Redis 的纯内存操作)。
- 需要维护 ZK 集群,复杂度更高。
4. 代码示例(Curator 框架)
InterProcessMutexlock=newInterProcessMutex(client,"/myLock");if(lock.acquire(10,TimeUnit.SECONDS)){try{// 业务逻辑}finally{lock.release();}}Curator 封装了锁的细节,使用简单。
四、基于数据库的分布式锁
1. 悲观锁(select for update)
利用数据库的行锁:在事务内执行SELECT ... FOR UPDATE,其他线程的相同查询会被阻塞,直到事务提交或回滚。
BEGIN;SELECTidFROMorderWHEREid=1001FORUPDATE;-- 业务操作UPDATEorderSETstatus=1WHEREid=1001;COMMIT;- 优点:实现简单,依赖于数据库行锁。
- 缺点:性能差,容易死锁,数据库连接占用久,不适合高并发。
2. 乐观锁(版本号)
不使用显式锁,通过版本号(version)保证更新时的原子性。
UPDATEorderSETstatus=1,version=version+1WHEREid=1001ANDversion=old_version;如果更新影响行数为 0,表示已被其他线程修改,重试或失败。
- 优点:无阻塞,性能较好。
- 缺点:不支持强互斥(适合写冲突不频繁的场景),需要重试逻辑。
结论:数据库锁通常只用于轮询调度、简单后台任务等低并发场景,不推荐作为高并发分布式锁。
三者的对比
| 维度 | Redis | Zookeeper | 数据库 |
|---|---|---|---|
| 性能 | 最高(纯内存) | 中等 | 最低 |
| 一致性 | 最终一致(主从可能丢失) | 强一致(ZAB) | 强一致(ACID) |
| 锁安全性 | 需处理过期、续命 | 自动释放(临时节点) | 依赖事务超时 |
| 死锁风险 | 有(需设置过期时间) | 几乎无 | 有(事务未提交) |
| 实现复杂度 | 低(Redisson 封装良好) | 中(Curator 封装) | 低(SQL) |
| 典型场景 | 高并发缓存、秒杀 | 强一致性配置、选主 | 低并发后台任务 |
五、常见面试追问
Q1:Redis 锁的过期时间怎么设置?
- 应大于业务执行的最大时间 + 少量缓冲。如果时间不确定,使用看门狗自动续期。
- 过短:业务未完成锁就自动释放,导致并发问题。
- 过长:锁持有者挂了,其他线程长时间无法获取。
Q2:Zookeeper 锁的羊群效应是什么?如何避免?
如果没有对每个节点单独监听,所有等待锁的客户端都监听同一个父节点,当锁释放时,所有客户端同时被唤醒去竞争,造成瞬时压力。Zookeeper 锁正确实现是让每个客户端监听前一个顺序节点,这样只有下一个节点被唤醒,避免了羊群效应。
Q3:Redlock 能保证 100% 安全吗?
不能。Redlock 存在理论上的争议(例如时钟跳跃、多数节点同时重启等),且实现复杂。大多数业务单 Redis 实例 + 哨兵已足够,若对一致性要求极高,建议使用 Zookeeper。
Q4:为什么不直接用setnx和expire两条命令加锁?
因为不原子:如果setnx成功,但在执行expire前客户端崩溃,锁永不过期,造成死锁。必须用set key value nx ex一条命令。
Q5:可重入锁怎么实现?
- Redis:记录锁持有者和重入次数(可以用 Hash 结构)。
- Zookeeper:Curator 的
InterProcessMutex已支持可重入。 - 数据库:使用
ThreadLocal记录当前线程的重入次数。
六、选型建议
- 常规高并发系统(如秒杀、订单扣减):推荐Redis + Redisson(自动续期),性能好,代码简单。
- 对一致性要求极高(如金融分布式任务调度、选主):推荐Zookeeper,牺牲一点性能换取更强的一致性保证。
- 数据库锁只用于极低并发的内部工具,不推荐生产使用。
一句话记住:Redis 高性能定时续,ZooKeeper 强一致防死锁;数据库锁性能差,高并发请绕道走。
分布式锁是分布式系统的核心组件,选型时要结合业务对性能、一致性、可靠性的要求。希望这篇文章能帮你彻底掌握分布式锁的各种实现,从容应对面试和实际开发,欢迎继续讨论。