共享锁需显式声明(lock in share mode),用于读操作防并发修改;排他锁由update/delete或for update触发,保障读-改-写原子性;锁生效依赖索引,无索引则降级为表锁。

共享锁触发场景:读操作需要防止被并发修改
共享锁不是默认行为,只有显式声明才会加。它适用于“我正在读,且不希望别人改”的场景,比如校验库存、生成报表前锁定基础数据、读取配置项并准备后续业务判断。
常见错误现象:SELECT * FROM orders WHERE order_no = '2024001' 不加锁,其他事务可能在你读完后立刻 UPDATE 或 DELETE 这条记录,导致你基于过期数据做决策。
- 必须用
SELECT ... LOCK IN SHARE MODE显式加锁 - 只对 InnoDB 有效;MyISAM 不支持行级共享锁,会退化为表锁
- 若查询条件没走索引,InnoDB 会升级为全表扫描 + 全表共享锁,严重拖慢并发
- 事务未提交时,其他事务仍可加共享锁,但不能加排他锁(
SELECT FOR UPDATE或UPDATE会被阻塞)
排他锁触发场景:写操作或读-改-写流程必须独占数据
排他锁在两类情况下自动或显式触发:一是执行 UPDATE、DELETE、INSERT 时自动加;二是用 SELECT ... FOR UPDATE 显式申请,用于读-改-写原子性保障。
典型使用场景:扣减账户余额、抢购下单、更新订单状态。这些操作要求“读出当前值 → 计算新值 → 写回”,中间不能被其他事务干扰。
-
UPDATE accounts SET balance = balance - 100 WHERE id = 123自动加排他锁,无需额外语句 -
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE是安全起点——它把锁提前拿到,避免先读再 update 时被并发覆盖 - 如果
FOR UPDATE的 WHERE 条件无索引,同样会锁整张表,不是只锁一行 - 注意:普通
SELECT即使在事务里也不加锁,MVCC 提供快照读,不会阻塞别人
为什么有时候加了锁却没效果?关键在索引和引擎
锁是否生效、锁住几行,完全取决于执行计划是否命中索引。InnoDB 行锁本质是锁索引项,不是锁数据行本身。
容易踩的坑:SELECT * FROM users WHERE name = 'Alice' FOR UPDATE 如果 name 没建索引,InnoDB 只能全表扫描,最终锁的是整个聚簇索引——相当于表锁,所有并发写都会排队。
- 主键查询(如
WHERE id = 5)一定走聚簇索引,锁单行 - 唯一二级索引查询,会同时锁二级索引项 + 对应主键索引项
- 非唯一二级索引(如普通
INDEX(status)),可能锁多个索引项,甚至触发间隙锁(Gap Lock) - MyISAM 引擎下,
LOCK IN SHARE MODE和FOR UPDATE都无效,直接报错或静默忽略
并发冲突时会发生什么?看锁等待与超时
当两个事务竞争同一资源的不兼容锁时,后到者不会失败,而是进入等待状态,直到前一个事务释放锁或超时。
默认等待时间由 innodb_lock_wait_timeout 控制(通常 50 秒),超时后抛出错误:ERROR 1205 (40001): Deadlock found when trying to get lock 或更常见的 Lock wait timeout exceeded。
- 死锁检测是自动开启的,InnoDB 会主动 rollback 其中一个事务(通常选代价小的那个)
- 单纯锁等待不是死锁,只是串行化延迟;但若 A 等 B、B 等 A,就构成死锁
-
SHOW ENGINE INNODB STATUS能看到最近死锁详情,包括谁持有什么锁、谁在等什么 - 线上应避免长事务持有锁,比如在事务里调外部 HTTP 接口,极易引发大面积阻塞
真正决定锁行为的不是 SQL 写法本身,而是执行路径是否走索引、事务隔离级别是否启用 gap lock、以及引擎是否支持行级锁。没索引的 FOR UPDATE 和没索引的 UPDATE,实际效果一样危险。











