mysql中读写锁的使用与性能调优

来源:这里教程网 时间:2026-02-28 20:38:36 作者:

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
更容易拖垮系统。

相关推荐