编程 MySQL GET_LOCK 排查:metadata_locks 里 OWNER_THREAD_ID 不是连接 ID,重复取锁也不加行

2026-11-07 00:04:16

MySQL GET_LOCK 返回 0 还是 NULL:从 metadata_locks 看锁状态

GET_LOCK() 的返回值只有三种:1 / 0 / NULL,但 0 和 NULL 的分界很容易混淆:超时返回 0,被 KILL 或 OOM 返回 NULL。同一把锁在 performance_schema.metadata_locks 里的行数和状态,也跟“取了几次锁”对不上。

5.7.5 起由 MDL 子系统实现

用户锁不再是独立结构,而是 MDL 里的一类对象。打开 performance_schema.metadata_locks 可以看到:

  • OBJECT_TYPE='USER LEVEL LOCK'
  • OBJECT_NAME = 锁名
  • OBJECT_SCHEMA=NULL
  • LOCK_TYPE='EXCLUSIVE'
  • LOCK_DURATION='EXPLICIT'
SELECT OBJECT_NAME, OBJECT_TYPE, LOCK_TYPE, LOCK_DURATION,
       LOCK_STATUS, OWNER_THREAD_ID
FROM performance_schema.metadata_locks
WHERE OBJECT_TYPE = 'USER LEVEL LOCK';

先确认 instrument 开着

需要 wait/lock/metadata/sql/mdl 这个 instrument。5.7 默认可能没开:

UPDATE performance_schema.setup_instruments
SET enabled = 'YES'
WHERE name = 'wait/lock/metadata/sql/mdl';

或在 my.cnf 中:

performance_schema_instrument='wait/lock/metadata/sql/mdl=ON'

8.0 默认启用。

OWNER_THREAD_ID 不是连接 ID

这一列是 THREAD_ID,不是 CONNECTION_ID(),也不是 SHOW PROCESSLIST 里的 Id。要定位到会话并 KILL,必须 join performance_schema.threads 换算:

SELECT t.PROCESSLIST_ID, t.PROCESSLIST_USER,
       ml.OBJECT_NAME, ml.LOCK_STATUS
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads t
  ON t.THREAD_ID = ml.OWNER_THREAD_ID
WHERE ml.OBJECT_TYPE = 'USER LEVEL LOCK';

拿到的 PROCESSLIST_ID 才是 KILL 用的那个 Id。

递归取锁:行数 ≠ 取锁次数

同一会话可以对同名锁反复 GET_LOCK。只有第一次会在 metadata_locks 里建一行,后续调用只增加内部计数;最后一把释放时行才被删除。所以表里一行不代表只取过一次,反过来也不能用行数推算调用量。

等待者也有行

等待中的请求同样会出现在表里,LOCK_STATUS='PENDING',与持有者的 GRANTED 行并存。状态流转:

  • GRANTED:已持有
  • PENDING:等待中
  • TIMEOUT:等超时,随后被删除
  • KILLED:被 KILL 掉
  • VICTIM:死锁检测中被选中的牺牲者

表和 MDL 共用容量

metadata_locks 的容量由 performance_schema_max_metadata_locks 控制,用户锁与表、schema 的 MDL 共用同一张表。大表 DDL 加高并发用户锁时,会互相挤占位置。

返回值的实现细节

  • 1:成功
  • 0:超时。lock wait timeout 在 GET_LOCK 上不是错误,而是返回值
  • NULL:出错,例如 OOM 或被 KILL

服务端有 User_level_lock_wait_error_handler,专门捕获并压制 MDL 层的 timeout 错误,把它变成返回值 0;同时把 ER_LOCK_DEADLOCK 转成 ER_USER_LOCK_DEADLOCK。

死锁码 3058 不回滚事务

用户锁死锁报 ER_USER_LOCK_DEADLOCK(3058),这跟 InnoDB 的 1213 不同:它不会导致事务回滚。捕获到 3058 后事务仍然有效,需要自行决定是重试还是回滚。

timeout=0 也可能报死锁

Bug #87620:即使 timeout=0(本不该进入等待),也会走 MDL 的死锁检测从而报 deadlock。官方标注在 8.0.4 / 9.0.0 修复。

@@lock_wait_timeout 对用户锁无效

实现者的说明是:显式 timeout 参数优先,没有实现“与全局值取较小者”的逻辑。所以调 @@lock_wait_timeout 不会改变 GET_LOCK 的等待行为,必须在调用处写死超时。

存储函数里的超时不会中断语句

在存储函数或触发器里,GET_LOCK 超时返回 0 而不报错,语句继续往下执行。批量处理时这会造成部分行拿到锁、部分没拿到,中途没有任何异常提示。

被 KILL QUERY 或 KILL CONNECTION 中断等待时,GET_LOCK 返回 NULL,这一点和超时返回 0 要分开处理。

释放时机

  • 会话结束(正常或异常)自动释放
  • COMMIT / ROLLBACK 不释放

RELEASE_LOCK() 的返回值:

  • 1:释放成功
  • 0:不是本线程持有,不释放
  • NULL:锁不存在

RELEASE_ALL_LOCKS() 返回释放的数量。这两个函数在 binlog_format=STATEMENT 下不安全,会产生 warning。

命名规则

锁名服务端全局,最长 64 字符,建议用 db_name.str / app_name.str 这类带命名空间的形式,避免不同业务撞名。辅助查询:

SELECT IS_USED_LOCK('app_name.job');  -- 返回持有者 connection id
SELECT IS_FREE_LOCK('app_name.job');  -- 1 空闲 / 0 被占用

文档

推荐文章

程序员茄子在线接单