UPDATE返回0行但锁未释放,是因为WHERE匹配到行后InnoDB仍加X锁(即使Changed: 0),锁由事务生命周期控制而非语句执行结果;autocommit=0时漏COMMIT会导致锁长期悬挂。

UPDATE返回0行但锁没释放,是因为WHERE条件匹配到了行
影响行数为0 ≠ 没扫描行。InnoDB只要WHERE条件能定位到记录(哪怕新旧值完全一样),就会加X锁(排他锁)——这是为了保证可重复读隔离级别下的数据一致性。你看到Rows matched: 1, Changed: 0,说明它确实找到了那行,并锁住了,只是最终跳过了写入。
常见导致“匹配但未更新”的坑
这些情况都会让UPDATE命中行、加锁、但不修改数据:
-
WHERE条件正确,但SET的字段值与当前值完全一致(包括NULL = NULL不成立,得用IS NULL判断) - 字符串字段隐含尾部空格:
status存的是'shipped '(带空格),而SET status = 'shipped'会导致比较时被忽略,值“看似相同” - 时间戳精度差异:数据库存的是
2026-11-05 01:39:22.123456,你只赋了秒级NOW(),实际值不同但肉眼难辨 - 触发器在
BEFORE UPDATE中把NEW.status又设回了OLD.status,最终没变
锁是否释放,和autocommit、事务边界强相关
锁不是由UPDATE语句生命周期决定的,而是由事务生命周期控制的:
- 如果
autocommit = 1(默认),UPDATE执行完就自动提交,锁立刻释放——你根本来不及观察“锁没释放” - 如果
autocommit = 0,UPDATE在事务内执行,即使影响行数为0,锁仍会持续到COMMIT或ROLLBACK - 漏掉
COMMIT是线上最常被忽略的锁悬挂原因;查information_schema.INNODB_TRX能看到TRX_STATE = 'LOCK WAIT'或长时间TRX_STARTED的事务
如何验证是不是真锁住了某行?
别猜,直接用另一连接去竞争:
- 在第一个连接执行:
UPDATE t SET x = x WHERE id = 123;(确保该行存在) - 立刻在第二个连接执行:
SELECT * FROM t WHERE id = 123 FOR UPDATE; - 如果第二个连接卡住,说明第一连接确实在持有行锁——哪怕
row_count()返回0 - 再查
SELECT * FROM performance_schema.data_locks;(MySQL 8.0+)或SHOW ENGINE INNODB STATUS\G里的TRANSACTIONS部分,确认锁类型和持有者
锁住却没更新,是最容易被监控忽略的隐形阻塞点——因为SQL没报错、影响行数为0、慢日志里也不显眼。关键要意识到:加锁动作发生在“匹配”阶段,而非“变更”阶段。










