为什么 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,再好的索引也救不了。最容易被忽略的,是那些“看起来走索引了”的查询——只要它触发了间隙锁或锁升级,就已在高并发下埋了雷。
