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=NULLLOCK_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 被占用