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

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

为什么
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 = ?
),用乐观锁替代悲观锁

联合索引顺序怎么排,才能同时支撑
WHERE
ORDER 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>BETWEEN
IN
)最多只能有一个,且必须放在等值字段之后、排序字段之前
如果
ORDER BY
含多个字段,确保它们在索引中连续、方向一致;混合
ASC/DESC
在 MySQL 8.0 之前基本无法用索引排序

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

索引不是万能的——它加速定位,但不减少锁持有时间。如果

UPDATE
语句本身逻辑复杂(比如子查询、多表关联更新)、或被更新的行物理位置分散(导致大量随机 I/O),事务执行时间拉长,锁就一直挂着。

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

实操建议:

SHOW ENGINE INNODB STATUS\G
查看
TRANSACTIONS
部分,确认锁等待链中是否出现
waiting for table metadata lock
lock_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 UPDATE
REPLACE INTO
,它们的锁行为更可控
避免在唯一索引字段上做范围查询(如
WHERE phone LIKE '138%'
),极易锁住大片间隙
监控
innodb_row_lock_waits
innodb_row_lock_time_avg
,持续升高说明存在隐性锁竞争,光看慢日志发现不了
索引优化和锁优化从来不是两张皮:一个没设计好的索引,会让本该毫秒级的锁变成秒级等待;而一个没想清并发路径的 SQL,再好的索引也救不了。最容易被忽略的,是那些“看起来走索引了”的查询——只要它触发了间隙锁或锁升级,就已在高并发下埋了雷。

相关推荐