共享锁(lock in share mode)适用于需读取并防止他人修改但允许多方并发读的场景,如插入子表前校验父表记录存在;排他锁(select for update)用于“读-判-写”原子操作,确保后续更新前数据不被篡改,二者均须在显式事务中使用且依赖索引避免升级为表锁或间隙锁。

共享锁和排他锁不是“要不要用”的问题,而是“在哪加、加多久、加给谁”必须想清楚——加错位置或漏掉事务边界,锁就等于没加。
什么时候该用 LOCK IN SHARE MODE
它适用于“我需要读一行数据,并确保在后续操作完成前,没人能改它,但其他人还能继续读”的场景。典型例子是校验型读取:比如插入子表前,先确认父表记录存在且未被删除。
- 必须显式开启事务(
START TRANSACTION),否则锁在语句执行完立刻释放 - 只对满足 WHERE 条件的行加锁;没有索引时会升级为表锁,严重拖慢并发
- 其他事务仍可执行普通
SELECT(走 MVCC 快照),但不能执行UPDATE、DELETE或SELECT FOR UPDATE - 如果业务逻辑里紧接着要 INSERT 子记录,记得在同一个事务里做,否则父记录可能在你 INSERT 前就被删了
为什么 SELECT ... FOR UPDATE 不能只写在 UPDATE 前面就完事
很多人以为“先查再更新”自然就安全,但中间缺了锁,就等于裸奔。真正起作用的是 FOR UPDATE 把读和写绑进一个原子动作里。
-
FOR UPDATE加的是行级排他锁,不仅阻止其他事务修改,也阻止它们加任何锁(包括LOCK IN SHARE MODE) - 它只在当前事务提交或回滚后才释放,所以整个业务逻辑必须包在同一个事务中
- 如果 WHERE 条件不走索引,InnoDB 会锁住扫描到的所有行,甚至升级为间隙锁(Gap Lock),导致意外阻塞
- 注意:在可重复读(RR)隔离级别下,
FOR UPDATE还会隐式加间隙锁,防止幻读——这点常被忽略,却直接影响并发表现
UPDATE/DELETE 自动加排他锁,但别依赖它
确实,UPDATE 和 DELETE 语句执行时会自动加 X 锁,但这个锁只覆盖实际修改的行,而且只存在于语句执行期间——如果你需要基于查询结果做判断后再决定是否更新,那光靠 UPDATE 自带的锁完全不够。
- 例如:“余额大于 100 才扣款”,如果先
SELECT balance再UPDATE,中间窗口期可能被其他事务抢先扣减 - 正确做法是把判断逻辑放进 WHERE:
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100,再配合SELECT ... FOR UPDATE显式锁定 - 自动加锁不解决“读-判-写”三步之间的竞态,这是业务逻辑层必须补上的
容易被忽略的锁兼容性陷阱
锁不是孤立存在的,它和意向锁(IS/IX)、间隙锁共同构成锁体系。你以为只锁了一行,其实可能锁住了一段范围。
-
LOCK IN SHARE MODE和SELECT FOR UPDATE都会触发意向锁(IS 或 IX),而表级锁(如LOCK TABLES ... WRITE)会和所有意向锁冲突 - 在 RR 隔离级别下,即使 WHERE 条件命中唯一索引,
FOR UPDATE仍可能加间隙锁——比如查id = 5,但 id=4 和 id=6 之间没有记录,InnoDB 就会锁住 (4,6) 这个间隙 - 死锁往往不是因为锁太多,而是因为多个事务以不同顺序访问相同资源——比如事务 A 先锁 id=1 再锁 id=2,事务 B 反过来,就极易触发
真正难的从来不是语法怎么写,而是搞清每一行 SQL 背后 InnoDB 实际加了哪些锁、锁在什么粒度、持续到什么时候。看执行计划不够,得结合 INFORMATION_SCHEMA.INNODB_TRX 和 INNODB_LOCKS(MySQL 8.0+ 用 performance_schema.data_locks)实时观察。











