Skip to content

分布式锁

分布式锁是在分布式环境中实现互斥访问的机制,用于控制多个进程对共享资源的访问。分布式锁是分布式系统的基础组件,广泛应用于任务调度、库存扣减、幂等性保证。

为什么需要分布式锁

单机锁(如 Java 的 synchronized、ReentrantLock)只能在单机进程内有效,无法跨进程、跨机器。分布式系统需要分布式锁来协调多个进程的访问。

分布式锁的应用场景:任务调度(只有一个节点执行定时任务)、库存扣减(防止超卖)、幂等性保证(防止重复提交)、资源访问(控制并发访问量)。

分布式锁的设计要求

互斥性:同一时刻只有一个进程能获得锁。

容错性:锁服务故障时,仍能正常工作。锁服务需要集群部署,避免单点故障。

避免死锁:持有锁的进程崩溃,锁能够自动释放。需要设置超时时间或心跳续约。

可重入性:同一个进程可以多次获得同一个锁。需要记录锁的持有者。

高性能:获得锁和释放锁的操作要快。锁服务的性能要足够高。

基于 Redis 的分布式锁

SET NX EX

SET key value NX EX 命令可以设置一个带过期时间的 key,如果 key 不存在则设置成功,返回 OK,表示获得锁。如果 key 已存在则设置失败,返回 nil,表示锁已被占用。

释放锁时删除 key,但需要判断 key 的值是否是自己设置的,避免误删其他进程的锁。使用 Lua 脚本保证原子性:if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end。

SET NX EX 的问题

锁超时:如果业务执行时间超过锁超时时间,锁会自动释放,其他进程可以获得锁,导致多个进程同时持有锁。解决方案:设置合理的超时时间,或使用 watchdog 续约。

主从切换:Redis 主从异步复制,Master 持有锁后未同步到 Slave 就故障,Slave 升级为 Master 后,其他进程可以获得锁,导致多个进程同时持有锁。解决方案:使用 Redlock 算法。

单点故障:Redis 单点故障会导致锁服务不可用。解决方案:使用 Redis Sentinel 或 Redis Cluster。

Redlock 算法

Redlock 是 Redis 作者提出的分布式锁算法,使用多个独立的 Redis 实例(通常 5 个)。客户端依次获取所有实例的锁,如果多数实例获取成功,且获取时间小于锁超时时间,则认为获得锁。

释放锁时依次释放所有实例的锁。

Redlock 的问题:时钟跳跃问题,如果 Redis 服务器时钟回拨,可能导致锁提前过期。Redlock 的性能较差,需要与多个实例通信。

基于 ZooKeeper 的分布式锁

临时顺序节点

ZooKeeper 的临时节点会在客户端断开连接时自动删除,顺序节点保证节点名的有序性。分布式锁的实现:客户端创建临时顺序节点,如果序号最小,则获得锁。否则监听前一个节点的删除事件,当前一个节点删除时,尝试获得锁。

释放锁时删除节点,后一个节点会被通知,尝试获得锁。

ZooKeeper 锁的优势

容错性:ZooKeeper 集群通过 ZAB 协议保证一致性,即使部分节点故障,仍能正常工作。

避免死锁:临时节点会在客户端断开连接时自动删除,即使客户端崩溃,锁也能释放。

可重入性:可以在节点数据中记录锁的持有者和重入次数,实现可重入锁。

公平锁:临时顺序节点保证先来先得,实现公平锁。

ZooKeeper 锁的问题

性能:ZooKeeper 的写操作需要同步到多数节点,性能较低。创建和删除节点的开销较大。

复杂性:需要监听节点事件,处理连接断开重连,实现较复杂。

基于数据库的分布式锁

悲观锁

SELECT FOR UPDATE 语句可以对行加排他锁,其他事务无法修改该行。获得锁:开启事务,执行 SELECT FOR UPDATE,提交事务时释放锁。

悲观锁的问题:数据库压力大,锁表或死锁风险。

乐观锁

乐观锁通过版本号实现,更新时检查版本号是否变化。UPDATE table SET value = 1, version = version + 1 WHERE id = 1 AND version = 5。如果更新成功,则获得锁。如果更新失败,则版本号已变化,锁已被占用。

乐观锁的问题:并发高时冲突多,需要重试。

唯一索引

唯一索引可以保证只有一个插入成功。INSERT INTO lock (lock_name) VALUES ('my_lock') ON DUPLICATE KEY UPDATE。如果插入成功,则获得锁。如果插入失败(唯一索引冲突),则锁已被占用。

释放锁时删除记录。需要设置过期时间,避免死锁。

分布式锁的对比

实现方式优点缺点
Redis SET NX EX性能高、实现简单主从切换时可能失效
Redis Redlock容错性好性能差、时钟问题
ZooKeeper容错性好、避免死锁性能较差、实现复杂
数据库悲观锁实现简单性能差、锁表风险
数据库乐观锁无锁阻塞并发高时冲突多

分布式锁的最佳实践

设置合理的超时时间:超时时间要大于业务执行时间,但也不能太大,否则死锁时间过长。

使用心跳续约:如果业务执行时间不确定,可以使用 watchdog 线程定期续约,延长锁超时时间。

处理异常:业务代码要捕获异常,确保 finally 块中释放锁。

使用 Lua 脚本:Redis 的获取和释放操作要使用 Lua 脚本,保证原子性。

记录锁信息:锁的 value 可以记录客户端标识,便于排查问题。

监控锁持有时间:如果锁持有时间过长,可能是业务问题或死锁。

分布式锁的替代方案

串行化:将任务放入队列,由单个消费者依次执行。可以避免锁,但性能受限于单个消费者。

幂等性:设计幂等的业务逻辑,即使重复执行也不会出错。可以避免锁,但需要业务配合。

分段锁:将锁分段,减少锁冲突。例如库存扣减,可以按商品 ID 分段,不同商品可以并发扣减。

分布式锁是分布式系统的基础组件,理解分布式锁的原理和实现,有助于设计合适的并发控制方案。