数据库分布式锁的实现
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
在分布式系统中,多个服务实例运行在不同的JVM或物理机器上,本地锁(如synchronized、ReentrantLock)无法跨进程生效,若多个节点同时操作同一共享资源(如库存、订单、共享文件),会导致数据不一致、重复执行、超卖等问题。数据库分布式锁依托现有业务数据库,无需引入额外中间件,通过数据库的原子性操作和约束机制实现跨节点互斥,是最简单、成本最低的分布式锁实现方式之一,适用于中小规模、低并发的业务场景。
一、核心设计原则
实现数据库分布式锁需满足分布式锁的基本特性,同时规避数据库本身的性能和可靠性短板,核心原则如下:
- 互斥性:同一时刻,只有一个分布式节点能获取到锁,确保临界区代码唯一执行。
- 防死锁:避免节点获取锁后宕机、网络中断,导致锁永久无法释放,需设计超时自动释放机制。
- 安全性:节点只能释放自己持有的锁,禁止误删其他节点的锁,需通过唯一标识区分锁持有者。
- 可用性:数据库需支持主从/集群部署,规避单点故障,确保锁服务稳定可用。
- 可重入性(可选):同一节点的同一线程可多次获取同一把锁,避免自身死锁(需额外设计实现)。
二、主流实现方案(基于MySQL为例)
数据库分布式锁的核心是利用数据库的原子性操作(如插入、更新)和约束(唯一索引)实现互斥,主流方案分为3种,各有适用场景,可根据业务复杂度和并发量选择。
方案一:基于唯一索引的INSERT实现(最常用、推荐)
核心原理:创建分布式锁专用表,将锁标识(如资源名)设为唯一索引,多个节点同时尝试插入同一锁标识的记录时,只有一个节点能插入成功(获锁成功),其余节点因唯一索引冲突插入失败(获锁失败);释放锁时删除对应记录,若节点宕机未主动释放,通过过期时间实现锁自动失效,避免死锁。
1. 锁表结构设计(必建)
表结构需包含锁标识、过期时间、锁持有者唯一标识等字段,核心是为锁标识创建唯一索引,保证插入操作的原子性:
CREATE TABLE `distributed_lock` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `lock_key` VARCHAR(64) NOT NULL COMMENT '分布式锁标识(如:order:1001、stock:20,对应具体共享资源)', `client_id` VARCHAR(64) NOT NULL COMMENT '锁持有者唯一标识(如:服务IP+线程ID、UUID),用于防止误删锁', `expire_time` DATETIME NOT NULL COMMENT '锁过期时间(避免服务宕机导致死锁)', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '锁创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '锁更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_lock_key` (`lock_key`) -- 核心:唯一索引,保证同一lock_key只能插入一条记录 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT 'MySQL分布式锁表';
2. 核心操作(加锁、解锁、续期)
加锁:通过INSERT语句尝试插入锁记录,插入成功即获锁,失败则表示锁已被占用。需指定合理的过期时间(大于业务最大执行耗时,如5~30秒),同时传入客户端唯一标识:
-- 尝试获取锁:lock_key为资源标识,client_id为客户端唯一标识,expire_time为当前时间+过期时长(示例:5秒)
INSERT INTO distributed_lock (lock_key, client_id, expire_time)
VALUES ('stock:100', '192.168.1.100:8080:thread123', DATE_ADD(NOW(), INTERVAL 5 SECOND));
程序逻辑:执行INSERT后,若受影响行数=1,则获锁成功,执行业务逻辑;若抛出唯一索引冲突异常(Duplicate entry),则获锁失败,可选择重试、放弃或阻塞等待。
解锁:业务执行完成后,通过DELETE语句主动释放锁,需同时校验lock_key和client_id,避免误删其他节点的锁:
-- 释放锁:仅删除当前客户端持有的对应锁标识的记录 DELETE FROM distributed_lock WHERE lock_key = 'stock:100' AND client_id = '192.168.1.100:8080:thread123';
锁续期(可选):若业务执行时间可能超过锁过期时间,需在业务执行过程中定期续期(如每2秒更新一次expire_time),避免锁提前释放:
-- 锁续期:延长当前客户端持有锁的过期时间(示例:续期5秒) UPDATE distributed_lock SET expire_time = DATE_ADD(NOW(), INTERVAL 5 SECOND) WHERE lock_key = 'stock:100' AND client_id = '192.168.1.100:8080:thread123';
3. 优化:过期锁清理
若节点宕机未主动释放锁,锁会一直存在直到过期,可通过两种方式清理过期锁:
- 抢锁前清理:尝试加锁前,先删除对应lock_key的过期锁,再执行插入操作,避免过期锁占用资源:
-- 步骤1:清理该锁标识的过期锁
DELETE FROM distributed_lock
WHERE lock_key = 'stock:100'
AND expire_time < NOW();
-- 步骤2:尝试插入新锁
INSERT INTO distributed_lock (lock_key, client_id, expire_time)
VALUES ('stock:100', '192.168.1.100:8080:thread123', DATE_ADD(NOW(), INTERVAL 5 SECOND));
- 定时任务清理:部署全局定时任务(如每10秒),批量删除所有过期锁,减轻抢锁时的清理压力:
DELETE FROM distributed_lock WHERE expire_time < NOW();
4. 优缺点
优点:实现最简单、无额外组件依赖,开发成本低;依托唯一索引保证原子性,逻辑直观;支持锁自动过期,天然防死锁;可复用现有业务数据库,无需额外部署。
缺点:高并发下,大量节点同时插入会导致数据库IO压力大,性能较低(仅支持百级QPS);锁粒度较粗(基于lock_key),无法实现细粒度锁;存在锁竞争时,需自行实现重试机制,易出现活锁。
方案二:基于MySQL悲观锁(SELECT ... FOR UPDATE)
核心原理:利用MySQL InnoDB引擎的行级锁,通过SELECT ... FOR UPDATE语句锁定指定记录,其他节点尝试锁定同一记录时会被阻塞,直到当前节点释放锁(事务提交/回滚),实现互斥访问。适用于并发量较低、业务逻辑复杂、需要保证数据强一致性的场景。
1. 实现步骤
无需创建专用锁表,可复用现有业务表,或创建简单的锁表(仅需lock_key字段),核心是通过事务和行锁实现互斥:
-- 1. 开启事务(必须在同一个数据库连接中)
START TRANSACTION;
-- 2. 尝试获取锁:锁定指定lock_key的记录,FOR UPDATE会产生行级锁,其他节点会阻塞
-- 若记录不存在,可先插入再锁定(避免锁失效)
INSERT IGNORE INTO distributed_lock (lock_key, client_id, expire_time)
VALUES ('order:200', '192.168.1.101:8081:thread456', DATE_ADD(NOW(), INTERVAL 10 SECOND));
-- 锁定该记录,其他节点执行此语句会阻塞,直到当前事务提交/回滚
SELECT * FROM distributed_lock
WHERE lock_key = 'order:200'
FOR UPDATE;
-- 3. 执行业务逻辑(如订单创建、库存扣减)
-- ... 业务代码 ...
-- 4. 释放锁:提交事务,行级锁自动释放;若业务失败,回滚事务也会释放锁
COMMIT;
-- 异常场景:ROLLBACK;
2. 关键注意事项
- 锁与数据库连接强绑定:SELECT ... FOR UPDATE的锁仅在当前数据库连接的事务中有效,若连接断开(正常/异常),事务会自动回滚,锁随之释放,天然防死锁。
- 避免锁表:若查询条件未命中索引(如lock_key未建索引),FOR UPDATE会升级为表锁,导致所有锁操作阻塞,需确保lock_key字段有索引。
- 控制事务时长:事务执行时间越长,锁持有时间越长,会加剧并发阻塞,需尽量缩短事务耗时。
3. 优缺点
优点:数据一致性强,能有效防止并发修改;无需手动处理锁过期,事务结束自动释放;逻辑简单,无需复杂的锁清理机制。
缺点:高并发下阻塞严重,性能比方案一更差;锁粒度较粗,易产生死锁(如多个节点交叉锁定不同记录);依赖数据库事务,对数据库性能消耗较大。
方案三:基于MySQL函数GET_LOCK/RELEASE_LOCK
核心原理:MySQL自带专门的锁函数GET_LOCK(key, timeout)和RELEASE_LOCK(key),基于数据库连接(Session)实现分布式锁,无需创建专用锁表,适用于简单场景、快速验证业务逻辑。
1. 核心操作
加锁:调用GET_LOCK(key, timeout),尝试获取名为key的锁,timeout为超时时间(单位:秒),0表示立即返回,-1表示永久阻塞:
-- 尝试获取锁:key为锁标识,timeout=5秒(5秒内获取不到则返回0)
SELECT GET_LOCK('pay:300', 5);
返回值说明:1→获锁成功;0→超时未获锁;NULL→执行出错(如数据库连接异常)。
解锁:调用RELEASE_LOCK(key),主动释放锁,释放成功返回1,锁不存在返回0,不是当前连接持有返回NULL:
-- 主动释放锁:key与加锁时一致
SELECT RELEASE_LOCK('pay:300');
强制释放锁(异常场景):若需手动释放其他连接持有的锁,可先查询持有锁的连接ID,再通过KILL连接释放锁(需数据库管理员权限):
-- 步骤1:查询持有指定锁的连接ID
SELECT PROCESSLIST_ID FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE STATE = CONCAT('Waiting for release of advisory lock for key ', QUOTE('pay:300'));
-- 步骤2:KILL该连接(自动释放锁)
KILL 1234; -- 1234为查询到的连接ID
2. 优缺点
优点:实现最简单,无需创建锁表;锁与连接绑定,连接断开自动释放,无死锁风险;开发成本极低,适合快速验证。
缺点:不支持分布式集群(主从切换后,锁会丢失);锁标识仅支持字符串,无法存储额外信息;不支持可重入,同一连接多次调用GET_LOCK会覆盖原有锁;无锁续期机制,超时后自动释放,易导致业务中断。
三、常见问题与解决方案
1. 死锁问题
问题:节点获取锁后宕机、网络中断,无法主动释放锁,导致锁永久占用。
解决方案:所有方案均需设置锁过期时间(方案一的expire_time、方案二的事务时长、方案三的timeout),结合定时任务清理过期锁;方案二、三依托数据库连接/事务,连接断开自动释放锁,进一步规避死锁。
2. 误删他人锁问题
问题:节点A的锁过期后,业务未完成,此时节点B获取锁;节点A业务完成后,调用解锁操作可能误删节点B的锁。
解决方案:加锁时传入客户端唯一标识(如UUID、IP+线程ID),解锁时必须同时校验lock_key和client_id(方案一的DELETE语句、方案二的事务绑定),确保仅释放自身持有的锁。
3. 锁超时与业务执行时长不匹配
问题:锁过期时间设置过短,业务未执行完,锁已被释放,导致其他节点获取锁,破坏互斥性。
解决方案:合理设置过期时间(结合业务最大执行耗时+1~2秒缓冲);实现锁续期机制(方案一的UPDATE语句),在业务执行过程中定期延长锁过期时间;避免长事务(方案二)。
4. 数据库单点故障
问题:数据库为单节点时,节点宕机后,整个锁服务不可用,影响业务正常运行。
解决方案:数据库部署主从集群,开启主从同步,确保锁数据同步到从节点;主节点宕机后,切换到从节点,保障锁服务连续性;避免使用方案三(不支持主从切换)。
5. 高并发性能瓶颈
问题:数据库分布式锁依赖磁盘IO和事务,高并发下(千级以上QPS)会出现性能瓶颈,甚至拖垮业务数据库。
解决方案:高并发场景下,优先选择Redis、ZooKeeper分布式锁;若必须使用数据库锁,可优化:① 锁粒度细化(如将stock:all拆分为stock:1、stock:2);② 增加本地锁兜底(先获取本地锁,再获取分布式锁,减少数据库访问);③ 限制抢锁重试频率,避免频繁插入/查询。
四、方案对比与选型建议
唯一索引INSERT | 实现简单、无额外依赖、支持锁自动过期、可集群 | 高并发性能低、需手动处理续期和过期清理 | 中小规模业务、低并发、快速迭代(如后台管理系统) |
悲观锁(SELECT ... FOR UPDATE) | 数据一致性强、事务结束自动释放锁、防死锁 | 并发阻塞严重、性能差、易产生死锁 | 并发量低、业务逻辑复杂、强一致性要求(如金融对账) |
GET_LOCK/RELEASE_LOCK | 无需建表、实现最简单、无死锁风险 | 不支持集群、无续期、不支持可重入 | 简单场景、快速验证业务逻辑(如临时脚本) |
五、总结
数据库分布式锁的核心是利用数据库的原子性操作和约束实现跨节点互斥,其最大优势是无额外组件依赖、开发成本低,适合中小规模、低并发的业务场景。但受限于数据库的IO性能,高并发场景下不推荐使用,建议优先选择Redis(高性能)、ZooKeeper(强一致性)分布式锁。
使用数据库分布式锁时,需重点关注锁过期、误删锁、数据库高可用三个核心问题,通过合理设置过期时间、校验锁持有者、部署主从集群,可有效规避大部分风险,保障锁服务的稳定可靠。同时,即使使用了分布式锁,也应在业务层面设计幂等接口,防止锁失效时出现脏数据,进一步提升系统健壮性。
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
本专栏聚焦MySQL并发控制核心:锁机制与MVCC多版本并发控制。拆解行锁、表锁、意向锁、间隙锁、临键锁,详解MVCC的undo log、read view、版本链实现。讲透事务隔离、幻读、死锁、锁等待等高频考点与实战问题。助力后端开发者、DBA快速掌握高并发下数据一致性与性能调优,夯实面试与工程实践核心能力。
查看9道真题和解析