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