事务的隔离性靠锁和MVCC共同实现,不是“用了事务就自动上锁”,而是根据操作类型、隔离级别、SQL语句显式/隐式触发锁机制。
哪些SQL会真正加行锁?
很多人以为
SELECT一定不加锁,其实不然:在
REPEATABLE READ隔离级别下,普通
SELECT是快照读(走 MVCC,不加锁),但只要带上
FOR UPDATE或
LOCK IN SHARE MODE,就会立即申请行级排他锁(X锁)或共享锁(S锁)。
UPDATE/
DELETE语句,即使没命中索引,也可能升级为表锁或间隙锁(尤其是 WHERE 条件无法使用索引时)
INSERT会加插入意向锁(Insert Intention Lock),与间隙锁冲突——这是幻读防控的关键一环 全表扫描的
SELECT ... FOR UPDATE会为所有扫描过的记录加锁,甚至可能锁住不存在但符合范围的“间隙”(即间隙锁)
为什么 UPDATE
卡住,但 SELECT
不卡?
因为默认隔离级别下,
SELECT走的是 MVCC 快照读,它不争抢锁,只读自己事务启动时刻可见的版本;而
UPDATE必须获取 X 锁才能修改,一旦目标行已被其他未提交事务锁定,就会进入等待队列(
is_waiting=true)。 这种“读不阻塞写、写不阻塞读”的分离,是 InnoDB 高并发的基础 但注意:如果
SELECT加了
FOR UPDATE,它就变成当前读,会和
UPDATE互斥 用
SHOW ENGINE INNODB STATUS\G可查到锁等待链,比如
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
意向锁(IX/IS)不是摆设,它是表锁和行锁的“通行证”
你执行
SELECT ... FOR UPDATE时,InnoDB 先申请表级
IX锁,再尝试对具体行加 X 锁。这个
IX的作用不是锁数据,而是快速告诉别人:“这张表里有行正被写”。没有它,每次加行锁前都得扫全表检查有没有表锁,性能崩盘。
LOCK TABLES t WRITE会阻塞所有 IX/IS,所以它能彻底禁止其他事务访问该表 两个事务同时对同一张表的不同行加 X 锁,各自持 IX 锁完全不冲突——这就是多粒度锁的设计精妙处 误以为“没显式锁表就不会被阻塞”,结果一个 DDL(如
ALTER TABLE)来了,发现被
IX锁住了元数据锁(MDL),整个表卡住
MVCC 和锁不是二选一,而是按需协作
MVCC 解决的是“读-写并发”,但它不解决“写-写并发”——两个事务同时
UPDATE同一行,必然要靠 X 锁串行化。而 MVCC 的版本链依赖 undo log,一旦事务长时间不提交,undo log 不能回收,会导致历史版本堆积、空间暴涨、甚至阻塞 purge 线程。 长事务是锁和 MVCC 的双重敌人:既可能持有锁不放,又让 purge 停摆
READ COMMITTED下每次
SELECT都生成新 Read View,
REPEATABLE READ复用事务首次读的 Read View——这意味着后者能避免不可重复读,但幻读仍需间隙锁补位 想彻底规避锁竞争?业务层可考虑乐观锁(如
version字段 +
WHERE version = ?),但要注意它不防脏读,也不替代事务边界
真正容易被忽略的,是锁的“隐形成本”:锁结构本身占内存、等待队列调度有开销、死锁检测会暂停事务几毫秒。与其纠结“要不要加锁”,不如先看清楚——你的 SQL 走的是快照读还是当前读,是否命中索引,事务是否真的必要短平快。
