原创

MySQL GET_LOCK 双会话实验:连接锁、重入与连接池释放

COMMIT 已经成功,另一个请求却一直拿不到同名锁。排查事务时看不到行锁,重启连接池后问题又消失了。这类现象先检查代码有没有调用 GET_LOCK:它获得的是连接持有的命名锁,事务提交和回滚都不会释放。

这个功能可以协调同一 MySQL 实例上的合作任务,但连接生命周期必须受到控制。把它当成普通事务锁,或者以为连接池的 close() 一定断开物理连接,都会留下难以解释的串扰。

打开两个真实会话,先观察持有者

实验面向 MySQL 8.4,不需要创建业务表。打开两个 mysql 客户端窗口,分别记为 A、B,确保它们连接到同一实例。不要在会自动切换连接的 SQL 工具页签中混做实验。

会话 A:

SELECT CONNECTION_ID() AS connection_a;
SELECT GET_LOCK('billing:tenant42:2026-10', 2) AS acquired;
START TRANSACTION;
SELECT 1;
COMMIT;
SELECT IS_USED_LOCK('billing:tenant42:2026-10') AS holder;

首次获取预期返回 1。事务提交以后,持有者仍应是 A 的连接编号。这个锁并不依赖前面的 START TRANSACTION,特意加上事务只是为了验证两者的生命周期没有绑定。

会话 B:

SELECT CONNECTION_ID() AS connection_b;
SELECT GET_LOCK('billing:tenant42:2026-10', 0) AS acquired;

等待时间为零,表示立即尝试。锁仍被 A 持有时,B 应得到 0。这里必须区分三个结果:1 是拿到锁,0 是等待超时,NULL 是发生错误。把结果转成“非空就是成功”,或者只判断 SQL 没抛异常,都不正确。

同一连接可以重入,因此少释放一次也会泄漏

在会话 A 再获取一次同名锁,然后只释放一次:

SELECT GET_LOCK('billing:tenant42:2026-10', 0) AS reacquired;
SELECT RELEASE_LOCK('billing:tenant42:2026-10') AS released_once;

再到 B 尝试,仍可能拿不到,因为 A 的同名锁获取计数还没归零。回到 A 释放第二次:

SELECT RELEASE_LOCK('billing:tenant42:2026-10') AS released_twice;

此时 B 再试:

SELECT GET_LOCK('billing:tenant42:2026-10', 0) AS acquired;
SELECT RELEASE_LOCK('billing:tenant42:2026-10') AS released;

这组操作暴露了连接池里尤其危险的情况:上一个请求遗留了锁,下一个请求恰好借到同一物理连接,再次获取会成功,看起来互斥逻辑完全正常。直到另一个连接参与竞争,问题才出现。

因此,日志里的“获取成功”不足以证明锁没有泄漏。需要同时记录操作标识、锁名、连接编号、获取结果和释放结果,而且不应把业务密钥或用户隐私直接塞进锁名。

连接池归还不等于数据库会话结束

GET_LOCK 在明确释放或会话结束时释放。连接池通常把逻辑连接归还到空闲队列,而不是每次关闭底层 TCP 连接。事务管理器执行的回滚、自动提交状态恢复,也不能被假定会顺带清理命名锁。

一段可靠的调用必须在同一物理连接上完成获取、业务动作和释放。业务 SQL 如果又通过另一个 DAO 获取新连接,至少要说清楚锁到底保护什么;如果代码在任意异常分支提前归还连接,释放逻辑就失去了持有者。

可以把流程约束为:借出连接后获取锁;确认返回 1 才进入工作;在 finally 中使用同一连接释放;核对释放结果;最后才归还连接。发生网络断连或释放结果不确定时,应按连接池支持的方式淘汰有疑问的物理连接,而不是把它继续交给下一位调用者。

RELEASE_ALL_LOCKS() 能清掉当前会话的命名锁,但不适合被公共工具方法随意调用。该连接可能还有另一层业务合法持有的锁,粗暴全部清理会提前解除别人的协调。更好的设计是明确每一层拥有的锁名和获取次数,避免无意重入。

连接池可能提供连接重置或销毁接口,但是否使用、在哪个异常分支触发,都要核对具体实现和配置。不能把某次测试碰巧断开连接的现象,当成所有归还路径的保证。

命名和等待策略也属于协议

命名锁在同一服务端范围内协调,不按数据库自动隔离。同名的两个应用可能互相阻塞,因此锁名应该带稳定业务前缀和资源标识。MySQL 8.4 对名字长度有上限,不能直接拼接无界请求字符串;需要压缩时使用稳定摘要,并确保所有参与者采用同一种编码和规范化规则。

等待时间应受请求总预算约束。一个两秒接口在这里等三十秒,后面还有事务、网络和下游动作,超时只是被推迟暴露。没有拿到锁的调用应返回明确的忙碌或重试结果,不能继续执行被保护的动作。

多个命名锁也可能形成死锁。若一次业务需要锁住多个资源,按确定顺序获取,失败时释放已经获得的锁。不要把“数据库检测到了锁死锁”理解成业务事务已经替你回滚和清理所有状态,异常分支仍要完整收尾。

它保护的是合作,不是实例外的所有写者

这个锁只影响同一实例上同名的获取操作,不会禁止绕过协议直接修改表的连接。数据库切主后,新实例也不会自动继承旧实例的命名锁状态。老工作者若仍在执行外部写入,另一个实例上的锁不能替它建立跨实例隔离。

所以资金扣减、库存正确性或任务幂等仍应由相应数据库约束、条件写入和业务提交协议保障。命名锁可以降低合作任务同时运行的概率,不能替代持久化的完成记录,更不能因为“已经有锁”就删除唯一约束。

实验结束,在每个会话核对自己持有的锁已释放,再退出客户端。这个实验最值得带回代码评审的问题是:获取和释放是否真的落在同一个连接上,归还连接以前是否能证明这次获取已经收尾。

正文到此结束
评论插件初始化中...
Loading...