MySQL中锁由事务生命周期控制,COMMIT/ROLLBACK才释放;隔离级别决定加锁类型与范围;死锁源于加锁顺序不一致;InnoDB行锁依赖索引,无索引则退化为表锁。

事务提交/回滚才真正释放锁
MySQL 中锁不是“用完即放”,而是由事务生命周期严格控制的。InnoDB 默认在 COMMIT 或 ROLLBACK 时才释放所有行锁、间隙锁、意向锁;即使一条 UPDATE 执行完了,只要事务没结束,锁就还占着。这意味着:一个长事务(比如含复杂计算或外部 API 调用)会持续持锁,极大拖慢其他并发请求。
- 显式开启事务后,
BEGIN或START TRANSACTION就进入锁敏感期,后续任何 DML 都可能加锁 - 自动提交模式(
autocommit=1)下,单条语句本身就是独立事务,锁生命周期极短——这是高并发读写的默认安全基线 - 若误设
SET autocommit = 0后忘记COMMIT,锁会一直挂着,直到连接断开或超时(由wait_timeout控制),此时查information_schema.INNODB_TRX能看到trx_state = 'RUNNING'却无活跃 SQL
隔离级别决定“加什么锁”和“什么时候加”
锁不是孤立存在的,它被事务的隔离级别直接调度。比如同样执行 SELECT * FROM account WHERE id = 10:
- 在
READ COMMITTED下,InnoDB 用快照读(MVCC),不加锁;但UPDATE仍会对匹配行加 X 锁 - 在默认的
REPEATABLE READ下,普通SELECT仍是快照读,但如果你加了SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE,就会立刻加行锁+间隙锁(防幻读) -
SERIALIZABLE会把所有普通SELECT都转成LOCK IN SHARE MODE,强制加 S 锁——几乎等于串行化,极少在生产使用
关键点:**不是“事务用了锁”,而是“事务按隔离级别策略决定要不要锁、锁多大范围”。** 比如 REPEATABLE READ 下的唯一索引等值查询只锁一行,但范围查询(WHERE id > 5)会锁住间隙,甚至可能锁住不存在的“下一个键”。
死锁不是锁太多,而是加锁顺序不一致
两个事务各自持有资源又等待对方释放,就死锁了。典型场景是更新不同行但顺序相反:
事务 A:UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;事务 B:UPDATE users SET balance = balance - 100 WHERE id = 2;
UPDATE users SET balance = balance + 100 WHERE id = 1;InnoDB 检测到循环等待后,会选一个事务回滚(报错 ERROR 1213 (40001): Deadlock found when trying to get lock),另一个继续。这不是 bug,是正常保护机制。
- 避免方法很简单:所有业务逻辑统一按主键升序(或固定字段顺序)更新多行,比如总是先 update id 小的,再 update id 大的
- 不要在事务里做非数据库操作(如发 HTTP 请求、写文件),否则锁持有时间不可控,死锁概率飙升
- 监控死锁:定期查
SHOW ENGINE INNODB STATUS\G的LATEST DETECTED DEADLOCK区块,看哪些表、哪些索引卷入了
表锁 vs 行锁:引擎决定底线能力
锁粒度根本上由存储引擎决定,不是 SQL 写法能绕过的:
- MyISAM 只支持表锁——哪怕你只改一行,整张表都写阻塞。所以它根本不支持事务,
START TRANSACTION在 MyISAM 表上只是个空壳 - InnoDB 默认行锁,但前提是走了索引。如果
UPDATE t SET a=1 WHERE b=5中b没索引,InnoDB 会退化为锁全表(实际是锁所有聚簇索引记录,效果等同表锁) -
LOCK TABLES ... WRITE是显式表锁,会彻底阻塞所有其他线程对这张表的读写,连SELECT都要等——这和事务内隐式加锁完全不是一回事,慎用
最容易被忽略的是:**没有索引的 WHERE 条件,会让 InnoDB 的“行锁”失效。** 别只盯着事务是否开启,先确认执行计划里有没有 type: ALL 或 key: NULL。










