锁由事务生命周期控制,语句执行完不立即释放:RC/RC下语句级释放行锁(间隙锁除外),RR下持续至事务结束;显式加锁和自动提交单DML亦遵循此规则。

事务提交/回滚时,锁到底什么时候释放?
MySQL 中事务和锁是强绑定关系:**锁由事务持有,也由事务生命周期控制释放时机**。不是语句执行完就放锁,也不是事务一开启就全加锁——关键看隔离级别和具体语句。
-
READ COMMITTED和READ UNCOMMITTED下,InnoDB 通常在语句执行完就释放行锁(非唯一条件扫描可能保留间隙锁); -
REPEATABLE READ(InnoDB 默认)下,行锁会持续到事务结束(COMMIT或ROLLBACK),包括 next-key 锁(行锁 + 间隙锁),这是可重复读不出现幻读的底层保障; -
SELECT ... LOCK IN SHARE MODE和SELECT ... FOR UPDATE显式加锁,锁一定撑到事务结束; - 自动提交模式(
autocommit=1)下,单条 DML 本质是独立事务,锁在语句执行完即释放——这点极易被忽略,导致误以为“没加事务就安全”,其实照样阻塞别人。
为什么加了事务,UPDATE 还是没锁住某行?
常见错觉:“我写了 START TRANSACTION; UPDATE ...; ,肯定锁住了”。但实际是否上锁、锁什么、锁多宽,取决于 WHERE 条件是否命中索引、是否唯一、是否走范围扫描。
- WHERE 条件未命中任何索引 → InnoDB 退化为全表扫描 → 对所有扫描过的行加锁(甚至可能升级为表锁,尤其在低版本或特定配置下);
- WHERE 是非唯一二级索引(如
status=1)→ 不仅锁匹配的行,还会锁索引间隙(gap lock),防止幻读; - WHERE 是主键或唯一索引等值查询(如
id=100)→ 只加 record lock(精确行锁),不锁间隙; - UPDATE 没匹配到任何行 → 仍可能加 gap lock(比如
WHERE id=999但该 ID 不存在),用于防止其他事务插入该位置,这点常被当成“无操作就不锁”而踩坑。
SELECT FOR UPDATE 在高并发下为什么会卡住?
SELECT ... FOR UPDATE 是典型的悲观锁语句,它不只是“查”,而是立刻申请排他锁(X lock)。卡住的本质是锁等待队列阻塞,不是 SQL 慢。
- 若前一个事务已对同一行持 X 锁且未提交,当前事务会进入锁等待状态,直到超时(默认
innodb_lock_wait_timeout=50秒)或被唤醒; - 多个事务按不同顺序更新两行(如事务 A 先锁 row1 再锁 row2,事务 B 先锁 row2 再锁 row1)→ 极易触发死锁,InnoDB 会自动检测并回滚其中一个(报错
Deadlock found when trying to get lock); - 避免卡死的关键不是减少使用,而是:统一加锁顺序、缩短事务时间、用
SELECT ... LOCK IN SHARE MODE替代(如果后续只是读+校验)、必要时加重试逻辑; - 注意:
FOR UPDATE在READ COMMITTED下只锁命中的行,但在REPEATABLE READ下还锁间隙,影响面更大。
事务里混用 SELECT 和 UPDATE,锁行为怎么算?
很多人以为“先 SELECT 再 UPDATE 就安全”,其实锁策略完全由最终执行的语句决定,SELECT 本身在默认隔离级别下不加锁(快照读),除非显式加锁。
- 普通
SELECT(无FOR UPDATE/LOCK IN SHARE MODE)→ InnoDB 使用 MVCC,读的是事务开始时的快照,不加任何锁(非阻塞读); - 但如果在
REPEATABLE READ下,先SELECT ... FOR UPDATE查一行,再UPDATE同一行 → 第二个 UPDATE 不会重复加锁,而是复用已持有的 X 锁; - 更危险的是:先普通 SELECT 得到 ID,再用该 ID 去 UPDATE —— 这中间存在窗口期,其他事务可能已改/删该行,导致 UPDATE 影响 0 行或业务逻辑异常(需配合
version字段或SELECT ... FOR UPDATE一起查); - 真正要保证一致性,必须把“读-判-改”包在一个显式事务中,并对关键行显式加锁,不能依赖两次独立语句的“逻辑连续性”。
FOR UPDATE,只要事务里有 DML,InnoDB 就已在后台悄悄加锁;而看似无害的普通 SELECT,一旦脱离 MVCC 快照语义(比如用了 SELECT ... FOR UPDATE),就会立刻暴露并发冲突。









