MySQL 中 SELECT ... FOR UPDATE
和 SELECT ... LOCK IN SHARE MODE
的适用场景
这两个语句是 InnoDB 实现行级读写锁的核心手段,但它们不是“加锁就能用”,而是必须在事务中生效,且只对索引列有效。如果
WHERE条件未命中索引,InnoDB 会退化为表锁(或间隙锁组合),导致并发骤降。
SELECT ... FOR UPDATE:适用于需要“先查后改”的典型场景,比如扣减库存、转账前校验余额——它会加排他锁(X 锁),阻塞其他事务的读写
SELECT ... LOCK IN SHARE MODE:适合多事务需协同读取同一数据但不修改的场景,比如生成报表时防止基础数据被删——它加共享锁(S 锁),允许其他事务加 S 锁,但阻塞 X 锁 两者都要求事务隔离级别为
READ COMMITTED或
REPEATABLE READ;在
READ UNCOMMITTED下会被忽略
为什么 FOR UPDATE
有时锁住整张表而不是某几行
根本原因在于锁的粒度由执行计划决定,而非 SQL 表面写法。即使写了
WHERE id = 123,若
id列没有索引,优化器无法定位具体行,只能走全表扫描,进而对所有聚簇索引记录加 X 锁——表现就是“锁表”。 用
EXPLAIN检查执行计划:
type字段必须是
const、
ref或
range,不能是
ALL或
index注意隐式类型转换:比如
WHERE user_id = '123'(字段是
INT),会导致索引失效,锁范围扩大 唯一索引和普通索引行为不同:非唯一索引可能触发间隙锁(Gap Lock),锁住不存在的值区间,造成意外阻塞
高并发下锁等待超时与死锁的实际应对
Lock wait timeout exceeded和
Deadlock found when trying to get lock是线上最常遇到的两类锁问题,但它们成因和处理方式完全不同。 锁等待超时(
Lock wait timeout exceeded):通常是事务持有锁时间过长(比如事务里混入 HTTP 调用、日志写入或 sleep),应缩短事务生命周期,把非数据库操作移出事务块 死锁(
Deadlock found when trying to get lock):本质是循环等待,MySQL 会自动回滚代价小的事务。关键不是避免死锁(不可能完全避免),而是让应用能捕获
errno 1213并重试——尤其对幂等性操作(如更新状态)必须支持重试逻辑 通过
SHOW ENGINE INNODB STATUS\G查看最近死锁详情,重点关注
TRANSACTION块中的
lock_mode X locks rec but not gap等描述,定位哪几行/哪个索引被争抢
替代方案:用 INSERT ... ON DUPLICATE KEY UPDATE
或 REPLACE INTO
规避显式锁
当业务逻辑本质是“存在则更新,不存在则插入”,硬上
SELECT ... FOR UPDATE+
INSERT/UPDATE不仅代码冗长,还放大锁竞争。这类场景优先考虑原子性更强的替代语法。
INSERT ... ON DUPLICATE KEY UPDATE:依赖唯一索引(主键或
UNIQUE约束),整个操作由 InnoDB 内部加锁完成,无需手动控制事务顺序,性能更稳
REPLACE INTO:底层是
DELETE + INSERT,会触发两次索引查找和锁操作,且可能引发自增 ID 跳变,在高并发下不如前者可控 注意:这两者仍会加锁,但锁的持有时间极短(仅在索引查找和行修改瞬间),且无事务上下文依赖,天然规避了长事务锁表风险
INSERT INTO order_lock (order_id, status, updated_at) VALUES (1001, 'processing', NOW()) ON DUPLICATE KEY UPDATE status = 'processing', updated_at = NOW();锁的复杂性不在语法本身,而在于它和索引、事务、隔离级别、执行路径深度耦合。一个没走索引的
FOR UPDATE,比十个没加锁的
UPDATE更容易拖垮系统。
