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

13次阅读

selectfor UPDATE 用于先查后改场景(如扣库存),加排他锁;SELECT … LOCK IN SHARE MODE 用于协同只读场景(如报表),加共享锁;二者均需事务、索引支持,否则退化为表锁或间隙锁。

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

mysqlSELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE 的适用场景

这两个语句是 InnoDB 实现行级读写锁的核心手段,但它们不是“加锁就能用”,而是必须在事务中生效,且只对索引列有效。如果 WHERE 条件未命中索引,InnoDB 会退化为表锁(或间隙锁组合),导致并发骤降。

  • SELECT ... FOR UPDATE:适用于需要“先查后改”的典型场景,比如扣减库存、转账前校验余额——它会加排他锁(X 锁),阻塞其他事务的读写
  • SELECT ... LOCK IN SHARE MODE:适合多事务需协同读取同一数据但不修改的场景,比如生成报表时防止基础数据被删——它加共享锁(S 锁),允许其他事务加 S 锁,但阻塞 X 锁
  • 两者都要求事务隔离级别为 READ COMMITTEDREPEATABLE READ;在 READ UNCOMMITTED 下会被忽略

为什么 FOR UPDATE 有时锁住整张表而不是某几行

根本原因在于锁的粒度由执行计划决定,而非 SQL 表面写法。即使写了 WHERE id = 123,若 id 列没有索引,优化器无法定位具体行,只能走全表扫描,进而对所有聚簇索引记录加 X 锁——表现就是“锁表”。

  • EXPLaiN 检查执行计划:type 字段必须是 constrefrange,不能是 ALLindex
  • 注意隐式类型转换:比如 WHERE user_id = '123'(字段是 int),会导致索引失效,锁范围扩大
  • 唯一索引和普通索引行为不同:非唯一索引可能触发间隙锁(Gap Lock),锁住不存在的值区间,造成意外阻塞

高并发下锁等待超时与死锁的实际应对

Lock wait timeout exceededDeadlock 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 STATUSG 查看最近死锁详情,重点关注 TRANSACTION 块中的 lock_mode X locks rec but not gap 等描述,定位哪几行/哪个索引被争抢

替代方案:用 INSERT ... ON DUPLICATE KEY UPDATEREPLACE 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 更容易拖垮系统。

text=ZqhQzanResources