数据库分布式锁的实现

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 文章被收录于专栏

本专栏聚焦MySQL并发控制核心:锁机制与MVCC多版本并发控制。拆解行锁、表锁、意向锁、间隙锁、临键锁,详解MVCC的undo log、read view、版本链实现。讲透事务隔离、幻读、死锁、锁等待等高频考点与实战问题。助力后端开发者、DBA快速掌握高并发下数据一致性与性能调优,夯实面试与工程实践核心能力。

全部评论

相关推荐

03-11 20:19
已编辑
门头沟学院 Java
太压力了,面了2个多小时,本菜比已经被拷打的瑟瑟发抖面完两个小时后通知过了1.算法题三道(1)leetcode124&nbsp;二叉树中最大路径和hard题&nbsp;因为不久前才刷过撕出来了,又来了一道(2)leetcode&nbsp;300&nbsp;最长递增子序列变种除了递增之外还加了一个权重因素,但是思路没变,dp就行(3)寻找词汇库里符合固定长度前缀的匹配单词应该是他们自己题库的题。给了一串单词列表,然后又给了一个单词,一个下标,根据这个下标的前缀去单词列表里面找到所有匹配的单词再返回思路是创建一个单词前缀树,然后根据树找,但是可能是构件树数有问题没撕出来2.全方位项目拷打基本没有问八股,全部都是项目企业场景题,哎哟我操,完全不会。我就纯八股战士,结果没想到一道八股都没问反正尽可能把企业场景往八股上引吧。。1.&nbsp;微服务多点部署其中一个宕机了怎么办2.&nbsp;要是mq占据大量CPU该怎么排查?MySQL占据大量CPU该怎么排查?3.&nbsp;假如说让你实现视频点赞功能,你打算怎么设计?讲讲思路(我知道多级缓存,但是碰巧没背……寄)4.&nbsp;Redis延迟双删是什么,分布式锁,哨兵模式5.&nbsp;MySQL到es同步的延迟该怎么优化6.&nbsp;Rabbit&nbsp;mq的队列是怎么实现的?(这个完全没整明白,可能是队列的底层结构?&nbsp;反正我硬扯的讲了一下rabbit&nbsp;mq的架构)还扯了很多,但是往后完全就慌了),记住的是这些
不知道怎么取名字_:2小时确实有压力,持续性的脑力劳动啊
查看9道真题和解析
点赞 评论 收藏
分享
一共一个小时,面试难度以及自己的回答算是最近的面试压力比较大的,实习问了30分钟,中间穿插八股。1.redis数据结构2.redis持久化机制3.mysql索引底层4.聚簇索引与非聚簇索引5.索引优化6.索引失效7.mysql执行一条sql8.那么多索引mysql怎么选(不会)9.tcp与udp区别10.tcp为什么可靠11.消息队列作用12.kafka怎么保证消息有序性13.mcp是什么?14.skills是什么?15.jvm内存分配与回收过程(我讲了从创建对象到判断垃圾对象到垃圾回收我全说了一遍,是这个吗?)16.fullgc触发机制17.tcp的拥塞控制流程(不会了)18.分布式事务解决方案,说了2pc,3pc,tcc。算法是反转双向链表,没有按格式输出,但是面试官没让继续写了,面完以为挂了,结果晚上秒过,看看复试什么情况吧。今天百度打电话准备发offer了,业务跟在手子的差不多,很垂,并且说不分日常暑期,只看表现,会有转正机会,但是考虑再三还是拒绝了,百度实习薪资确实有点低,title也不如之前了,但是面试的二位业务老师我很喜欢,对我的评价也不错,希望之后能有机会共事。从三月份到现在一共面了六家,面试次数总共是8场,情况如下:脉脉二面(无答复,默认挂)百度二面已oc美团一面过,下周一二面shein一面过直接HR面游族一面过直接HR面腾讯一面过等待约二面滴滴明天一面面试通过率还是蛮高的,但是大部分都是日常,感觉对我现在的加成不大,大概率不会去,不知道暑期会是什么情况呢唉,希望能有面试吧,继续加油。字节被无hc直接取消了,现在还没人捞,有没有字节HR救救我
不管什么都不想跳动了:本人美团百度快手都待过,建议肯定是直接留快手多一点产出后转正or直接冲字节腾讯暑期吧。一是快手从福利到基建都吊打另外两家。美团现在这个业务比较惨,本来毛利就很低,亏损严重,今年很可能要优化人力降低成本,去了别说日常,就算暑期后面都很可能被优化。百度其实实习生权限挺高的,可以接触到一些含金量高的项目,但是现在的风评不如之前了,薪资也不高。二是转正概率和薪资是跟产出挂钩的,你都在手子已经积累产出了,去其他家日常实习产出都是从0开始,肯定不可能有你在手子转正可能性大啊,现在日常压根没必要去,而且我有两个师弟都是在快手日常转正的,不用太担心,安心留在手子一边多做一点产出然后一边冲字节腾讯暑期,字节腾讯今年实习岗位非常多的,不如好好把握这个,加油。
今天你投了哪些公司?
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务