MySQL实现分布式锁的三种主流方案
作者:yangSnowy
前言:
MySQL 实现分布式锁的核心是利用数据库的原子性、唯一性约束或行级锁机制,保证分布式系统中多个节点对共享资源的互斥访问,解决跨进程、跨服务器的并发竞争问题。以下是三种主流、可靠且覆盖不同业务场景的实现方案,包含原理、完整实现、注意事项及适用场景对比,兼顾实用性和严谨性。
方案一:基于唯一索引的 INSERT 实现(最常用 / 推荐)
核心原理
利用 MySQL 唯一索引(UNIQUE INDEX)的唯一性约束实现锁的互斥:为分布式锁创建专用表,将锁标识(如资源名)设为唯一索引,多个节点同时尝试插入同一锁标识的记录时,只有一个节点能插入成功(获锁),其余节点因唯一索引冲突插入失败(抢锁失败);释放锁时删除该记录,若服务宕机未主动释放,可通过过期时间实现锁的自动释放,避免死锁。
1. 锁表结构设计(必建)
需包含锁标识(唯一)、过期时间、业务附加信息,同时为锁标识创建唯一索引,保证插入的原子性:
CREATE TABLE `distributed_lock` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `lock_key` VARCHAR(64) NOT NULL COMMENT '分布式锁标识(如:order:1001、stock:20)', `expire_time` DATETIME NOT NULL COMMENT '锁过期时间(避免服务宕机死锁)', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_lock_key` (`lock_key`) -- 核心:唯一索引,保证同一lock_key只能插入一条 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT 'MySQL分布式锁表';
2. 获取锁(INSERT 原子操作)
通过INSERT语句尝试插入锁记录,插入成功即获取锁,插入失败(唯一索引冲突)则表示锁已被其他节点占用。需指定合理的过期时间(大于业务执行的最大耗时,如 5 秒、10 秒):
-- 尝试获取锁:lock_key为具体资源标识,expire_time为当前时间+过期时长(示例:5秒)
INSERT INTO distributed_lock (lock_key, expire_time)
VALUES ('order:1001', DATE_ADD(NOW(), INTERVAL 5 SECOND));
程序处理逻辑:
执行 INSERT 后,若受影响行数 = 1 → 获锁成功,执行业务逻辑;
若抛出唯一索引冲突异常(或受影响行数 = 0)→ 获锁失败,可重试 / 放弃。
3. 释放锁(DELETE 主动释放)
业务执行完成后,主动删除对应 lock_key 的记录,释放锁供其他节点使用:
-- 释放锁:根据lock_key精准删除,避免误删其他锁 DELETE FROM distributed_lock WHERE lock_key = 'order:1001';
4. 锁超时自动释放(解决死锁)
若服务在执行业务时宕机 / 网络中断,无法主动执行 DELETE 释放锁,此时当expire_time小于当前时间,锁即失效。其他节点可通过先清理过期锁,再尝试获锁的逻辑优化抢锁流程:
-- 优化版获锁:先删除指定lock_key的过期锁,再插入(保证锁的可用性)
-- 步骤1:清理该lock_key的过期锁(通过定时脚本等)
DELETE FROM distributed_lock WHERE lock_key = 'order:1001' AND expire_time < NOW();
-- 步骤2:尝试插入新锁
INSERT INTO distributed_lock (lock_key, expire_time)
VALUES ('order:1001', DATE_ADD(NOW(), INTERVAL 5 SECOND));
关键注意事项
过期时间必须大于业务实际最大执行耗时,否则会出现 “业务未执行完,锁已过期被其他节点获取” 的并发问题;
释放锁时必须根据 lock_key 精准删除,禁止无条件 DELETE 或按 id 删除,避免误删其他节点的锁;
可通过 ** 乐观锁(加版本号)** 进一步优化,防止多个节点同时清理过期锁后重复插入(极端场景);
优点:实现简单、无侵入、支持集群(主从同步即可)、天然防死锁;缺点:高并发下可能存在轻微的锁竞争(可通过重试机制缓解)。
方案二:基于 MySQL 自带函数 GET_LOCK/RELEASE_LOCK
核心原理
MySQL 提供了专用的锁函数GET_LOCK(key, timeout)和RELEASE_LOCK(key),基于 ** 数据库连接(Session)** 实现分布式锁:
GET_LOCK(key, timeout):尝试获取名为key的锁,超时时间为timeout秒(0 表示立即返回,-1 表示永久阻塞);同一key只能被一个连接持有,其他连接尝试获取会阻塞 / 失败;
RELEASE_LOCK(key):主动释放名为key的锁,释放成功返回 1,锁不存在返回 0,不是当前连接持有返回 NULL;
若持有锁的连接断开(正常 / 异常),MySQL 会自动释放该连接持有的所有锁,天然防死锁。
1. 获取锁(GET_LOCK)
-- 尝试获取锁:key为锁标识,timeout=5秒(5秒内获取不到则返回0)
SELECT GET_LOCK('order:1001', 5);
返回值说明:
1 → 获锁成功;
0 → 超时未获取到锁;
NULL → 执行出错(如数据库连接异常)。
2. 释放锁(RELEASE_LOCK)
-- 主动释放锁:key与获取锁时一致
SELECT RELEASE_LOCK('order:1001');
3. 强制释放锁(针对异常场景)
若需手动释放其他连接持有的锁,可通过KILL连接实现(需数据库管理员权限):
-- 步骤1:查询持有指定锁的连接ID
SELECT PROCESSLIST_ID FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE STATE = CONCAT('Waiting for release of advisory lock for key ', QUOTE('order:1001'));
-- 步骤2:KILL该连接(自动释放锁)
KILL 1234; -- 1234为查询到的连接ID
关键注意事项
锁与数据库连接强绑定:若使用连接池,需保证获锁、执行业务、释放锁使用同一个连接(连接池不能中途回收连接);
不支持分布式集群(主从 / 多实例):GET_LOCK 的锁信息仅存储在当前 MySQL 实例的内存中,主从复制不会同步该锁信息,多实例场景下会出现锁失效;
锁粒度为字符串 key,支持任意自定义标识,实现简单;
优点:轻量(无需建表)、原子性强、自动释放锁;缺点:仅支持单机 MySQL、依赖数据库连接、无过期时间(除连接断开外)。
方案三:基于悲观锁(SELECT … FOR UPDATE)
核心原理
利用 MySQL InnoDB 引擎的行级排他锁实现分布式锁:通过SELECT … FOR UPDATE语句查询指定资源记录并加排他锁,多个节点同时执行该语句时,只有一个节点能获取到行锁(获锁成功),其余节点会被阻塞,直到锁被释放;锁的释放由事务提交 / 回滚控制,天然保证业务与锁的一致性。
1. 前置条件
必须使用InnoDB 引擎(MyISAM 不支持事务和行级锁);
被查询的字段必须创建索引(主键 / 唯一索引),否则会升级为表锁,导致并发性能急剧下降;
必须在显式事务中执行(START TRANSACTION / BEGIN),否则 SELECT 后会立即自动提交事务,锁被释放。
2. 获取锁(SELECT … FOR UPDATE + 事务)
先创建业务关联表(或使用现有业务表),确保查询字段有索引,然后在事务中加锁:
-- 示例:基于现有订单表实现锁,order_id为主键(索引) BEGIN; -- 开启显式事务 -- 尝试获取锁:查询指定order_id并加行级排他锁,无记录则返回空(可根据业务插入) SELECT * FROM `order` WHERE order_id = 1001 FOR UPDATE;
程序处理逻辑:
执行 SELECT 后,若成功返回记录 → 获锁成功,执行业务逻辑;
若被阻塞 → 等待其他节点释放锁,直到超时(由 innodb_lock_wait_timeout 参数控制,默认 50 秒);
若抛出锁等待超时异常 → 获锁失败,可重试 / 放弃。
3. 释放锁(事务提交 / 回滚)
业务执行完成后,提交事务释放行锁;若业务执行失败,回滚事务也会释放锁,避免死锁:
COMMIT; -- 业务成功,提交事务释放锁 -- ROLLBACK; -- 业务失败,回滚事务释放锁
关键注意事项
必须精准查询索引字段:若查询条件无索引(如 SELECT … FOR UPDATE WHERE name = ‘test’,name 无索引),会触发全表扫描并加表锁,导致所有节点阻塞,严重影响性能;
控制事务执行时长:事务未提交前,锁会一直持有,需避免在事务中执行耗时操作(如远程调用、文件 IO);
适用于业务与数据库操作强绑定的场景:锁的生命周期与事务一致,无需额外管理锁的释放;
优点:与业务表融合(无需单独建锁表)、行级锁并发性能高、事务一致性强;缺点:依赖事务、需关注索引设计、阻塞式等待(无非阻塞抢锁方式)。
通用设计原则(必遵守)
原子性:获锁、释放锁操作必须是原子的(MySQL 的 INSERT、GET_LOCK、SELECT … FOR UPDATE 均天然保证原子性),避免多节点同时获锁;
超时释放:必须设计锁超时机制(唯一索引的 expire_time、GET_LOCK 的连接断开、悲观锁的事务超时),杜绝死锁;
精准释放:释放锁时必须根据锁标识(lock_key / 资源 ID)精准操作,禁止批量删除 / 释放,避免误删其他节点的锁;
细粒度锁:锁的标识需尽可能细(如 order:1001 而非 order),减少锁竞争,提高并发性能;
异常处理:业务执行过程中若出现异常(如宕机、网络中断、超时),需保证锁能被自动释放,不影响后续节点抢锁;
重试机制:获锁失败时,可增加有限次数的重试逻辑(加随机延迟),避免瞬时竞争导致的业务失败,同时防止无限重试压垮数据库。
总结
首选方案:基于唯一索引的 INSERT 实现,兼顾实现简单、集群支持、防死锁,适配 90% 以上的分布式锁场景,是工业界主流选择;
轻量单机场景:选择GET_LOCK/RELEASE_LOCK,无需建表,操作便捷,但仅支持单机 MySQL;
业务与数据库强绑定场景:选择悲观锁 SELECT … FOR UPDATE,利用行级锁保证事务与锁的一致性,需重点关注索引和事务时长;
无论选择哪种方案,都必须遵守原子性、超时释放、精准释放三大核心原则,否则会出现锁失效、死锁、并发冲突等问题。
补充:MySQL 分布式锁适用于并发量中等、对性能要求不是极致的场景;若为超高并发(如每秒数万次抢锁),建议选择 Redis/ZooKeeper 分布式锁,性能和可靠性更优。
到此这篇关于MySQL实现分布式锁的三种主流方案的文章就介绍到这了,更多相关MySQL实现分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
