mysql索引优化中的并发查询与锁优化

2次阅读

selectfor update 在高并发下变慢主因是锁等待积;非唯一索引或索引失效会触发间隙锁/临键锁,锁住范围而非单行;应确保命中唯一索引、避免函数导致索引失效、优先用乐观锁;联合索引需按等值条件+范围条件+排序字段顺序构建,且排序字段须紧接等值字段后。

mysql索引优化中的并发查询与锁优化

为什么 SELECT ... FOR UPDATE 在高并发下会严重拖慢查询速度

这不是查询本身慢,而是锁等待堆积。InnoDB 的行锁在唯一索引(含主键)上才真正“锁住单行”;若查询条件走的是非唯一索引,或索引失效(如隐式类型转换、函数包裹列),SELECT ... FOR UPDATE 会升级为间隙锁(Gap Lock)甚至临键锁(Next-Key Lock),锁住一个范围——其他事务想插进这个范围、甚至只是查这个范围,都得等。

实操建议:

  • EXPLAIN 确认执行计划是否命中**唯一索引**;非唯一索引 + FOR UPDATE 是高并发锁冲突的常见源头
  • 避免在 WHERE 中对索引列使用 CAST()CONVERT()UPPER() 等函数,否则索引失效,触发全扫描+全表锁升级
  • 如果业务允许,把 FOR UPDATE 拆成先 SELECT(无锁读),校验通过后再 UPDATE + 原子条件(如 WHERE version = ?),用乐观锁替代悲观锁

联合索引顺序怎么排,才能同时支撑 WHEREORDER BY 并减少排序开销

mysql 只能利用索引的最左前缀匹配 WHERE 条件,而排序能否用上索引,取决于 ORDER BY 字段是否构成索引的“后缀连续段”,且方向一致(全部 ASC 或全部 DESC)。

比如要支持:WHERE user_id = ? AND status IN (?, ?) ORDER BY created_at DESC,索引应建为:(user_id, status, created_at),而不是 (user_id, created_at, status)

原因:status 是 IN 查询,属于范围扫描,它之后的字段(哪怕在索引里)无法用于排序;只有当 status 是等值(=)时,created_at 才能承接排序。

实操建议:

  • 把所有等值条件字段放最左,按过滤性从高到低排(如 user_id 通常比 status 区分度高)
  • 范围条件(>、<code>BETWEENIN)最多只能有一个,且必须放在等值字段之后、排序字段之前
  • 如果 ORDER BY 含多个字段,确保它们在索引中连续、方向一致;混合 ASC/DESC 在 MySQL 8.0 之前基本无法用索引排序

为什么加了索引,UPDATE 还是慢,并引发大量锁等待

索引不是万能的——它加速定位,但不减少锁持有时间。如果 UPDATE 语句本身逻辑复杂(比如子查询、多表关联更新)、或被更新的行物理位置分散(导致大量随机 I/O),事务执行时间拉长,锁就一直挂着。

更隐蔽的问题是:二级索引更新会触发聚簇索引(主键)的额外维护,以及唯一索引约束检查带来的隐式锁(如重复键检测需加 S 锁)。

实操建议:

  • SHOW ENGINE INNODB STATUSG 查看 TRANSACTIONS 部分,确认锁等待链中是否出现 waiting for table metadata locklock_mode X locks rec but not gap,这说明是行锁争用
  • 批量更新尽量用 WHERE id IN (?,?,?) 替代循环单条,但注意 IN 列表不宜过长(500 以内较安全),否则优化器可能放弃索引
  • 避免在事务中做耗时操作(如调用外部 API、大对象序列化),缩短事务生命周期才是锁优化的根本
SELECT    r.trx_id waiting_trx_id,   r.trx_mysql_thread_id waiting_thread,   r.trx_query waiting_query,   b.trx_id blocking_trx_id,   b.trx_mysql_thread_id blocking_thread,   b.trx_query blocking_query FROM information_schema.INNODB_LOCK_WAITS w INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;

唯一索引冲突时的锁行为,为什么 Duplicate entry 报错后还卡住后续插入

当插入违反唯一约束时,InnoDB 不仅要报错,还要确保这个“不存在”的值不会被其他事务趁机插入——所以会在冲突值对应的索引间隙上加插入意向锁(Insert Intention Lock),这是一种特殊的间隙锁。如果此时另一个事务正持有该间隙的间隙锁(比如前面的 SELECT ... FOR UPDATE 范围覆盖了这里),就会死锁或长时间等待。

这解释了为什么“明明没查到记录,INSERT 却卡住”。

实操建议:

  • 对高频插入的唯一字段(如订单号、手机号),优先用 INSERT ... ON DUPLICATE KEY UPDATEREPLACE INTO,它们的锁行为更可控
  • 避免在唯一索引字段上做范围查询(如 WHERE phone LIKE '138%'),极易锁住大片间隙
  • 监控 innodb_row_lock_waitsinnodb_row_lock_time_avg,持续升高说明存在隐性锁竞争,光看慢日志发现不了

索引优化和锁优化从来不是两张皮:一个没设计好的索引,会让本该毫秒级的锁变成秒级等待;而一个没想清并发路径的 SQL,再好的索引也救不了。最容易被忽略的,是那些“看起来走索引了”的查询——只要它触发了间隙锁或锁升级,就已在高并发下埋了雷。

text=ZqhQzanResources