第二个事务卡住是因为InnoDB对修改行加排他锁(X锁),而X锁与其他任何锁互斥;当id为主键时只锁单行,无索引则可能全表加锁或升级表锁。

UPDATE 同一行时为什么第二个事务会卡住
因为 InnoDB 对要修改的行加的是排他锁(X 锁),而 X 锁与任何其他锁(包括另一个 X 锁)都不兼容。当事务 A 执行 UPDATE t SET x = 1 WHERE id = 5 且 id 是主键时,InnoDB 立即在该行上加 X 锁;此时事务 B 尝试执行同样语句,发现目标行已被 X 锁占用,只能等待——直到事务 A 提交或回滚释放锁。
这和“有没有索引”强相关:
-
id是主键或唯一索引 → 只锁匹配的那 1 行(RECORD LOCK) -
WHERE条件无索引 → 可能全表扫描,每行都尝试加锁,甚至升级为表锁(尤其在READ COMMITTED以下隔离级别) - 使用非唯一索引 + 范围条件(如
WHERE status = 'pending')→ 可能加 NEXT-KEY LOCK(record + gap),但 gap 部分不阻塞其他索引路径的插入
注意:锁不是在 SQL 解析阶段加的,而是在存储引擎真正遍历到满足条件的记录时才申请。
SELECT ... FOR UPDATE 为什么有时不阻塞其他事务
SELECT ... FOR UPDATE 是否能阻塞后续 UPDATE,取决于它实际锁住了哪些数据——而这由执行计划决定,不是语句本身保证的。
常见不阻塞的情况:
- 查询条件没走索引,优化器选择全表扫描 → 可能只对扫描过程中“碰到”的行加锁,漏掉后续被其他事务插入或更新的行
- 在
READ COMMITTED隔离级别下,InnoDB 不加 GAP 锁 → 其他事务可在间隙中插入新行,而你的FOR UPDATE查不到它,自然也不锁它 - 查询用了覆盖索引且不回表 → 锁的是二级索引记录,而非聚簇索引行;若另一事务用主键更新同一逻辑行,可能绕过该锁(取决于是否触发唯一约束检查)
换句话说:FOR UPDATE 的效果 = 执行计划中的访问路径 × 隔离级别 × 索引结构。它不保证“把整条业务逻辑涉及的行都锁死”,只锁它实际读到的那些。
如何确认当前事务到底锁了什么
别猜,直接查。MySQL 提供两个可靠入口:
查正在运行的事务和锁持有情况:
SELECT * FROM information_schema.INNODB_TRX\G
关注TRX_ROWS_LOCKED(大概锁定行数)、TRX_MYSQL_THREAD_ID(线程 ID)、TRX_QUERY(当前语句)查锁等待关系:
SELECT * FROM information_schema.INNODB_LOCK_WAITS\G
能看到谁在等谁、等哪个锁、阻塞源是哪条语句更底层细节(含锁类型、内存地址):
SHOW ENGINE INNODB STATUS\G
输出里TRANSACTIONS和LOCK WAIT段落最实用
注意:INNODB_TRX 不会显示“未持有锁但事务未提交”的情况(比如只执行了 SELECT),这类事务虽不阻塞写,但可能拖长 undo log 生命周期,影响 purge 性能。
隐式事务没提交是隐形阻塞源
SQL Server 和 MySQL 都支持隐式事务(SET IMPLICIT_TRANSACTIONS ON),但很多人忽略它:
- 开启后,每个 DML(
UPDATE/INSERT)自动开启事务,但不会自动提交 - 如果忘了
COMMIT或ROLLBACK,X 锁一直挂着,后续所有想改同一行的事务都会卡住 - 这类事务在
INNODB_TRX里能看到TRX_STATE = 'RUNNING',但TRX_QUERY可能为空(因为语句已执行完,事务还开着)
排查建议:
- 应用层统一用显式事务(
BEGIN; ...; COMMIT;),禁用隐式模式 - 定期用
SELECT TRX_ID, TRX_STARTED, TRX_STATE FROM information_schema.INNODB_TRX WHERE TRX_STATE = 'RUNNING' AND TIME_TO_SEC(TIMEDIFF(NOW(), TRX_STARTED)) > 60扫描超时事务 - 监控
innodb_row_lock_waits状态变量突增,往往是隐式事务或长事务泄漏的信号
最麻烦的不是锁本身,而是锁背后那个忘了 COMMIT 的连接——它不报错、不中断,只安静地让别人排队。

















