应避免长事务导致锁等待堆积,需控制事务边界、监控并终止异常事务、合理设置隔离级别、优化索引与查询、拆分批量操作、规避隐式锁升级及元数据锁。

避免长事务导致锁等待堆积
长事务会持续持有锁,阻塞其他会话的读写操作,尤其在高并发场景下容易引发锁等待超时或死锁。mysql 的 innodb_lock_wait_timeout 默认是 50 秒,但实际业务中往往等不到那么久就失败了。
- 用
SHOW PROCESSLIST查看长时间运行的State为Locked或Waiting for table metadata lock的连接 - 在应用层控制事务边界:显式用
BEGIN/COMMIT包裹最小必要逻辑,避免在事务里做 rpc 调用、文件读写、用户交互等耗时操作 - 监控
INFORMATION_SCHEMA.INNODB_TRX表,重点关注TRX_STARTED和TRX_ROWS_LOCKED字段,及时 kill 异常长事务(KILL <code>TRX_MYSQL_THREAD_ID)
合理选择隔离级别与锁类型
不是所有业务都需要默认的 REPEATABLE READ。过高的隔离级别会增加锁范围和冲突概率,比如 select ... FOR UPDATE 在 RR 下可能加间隙锁(Gap Lock),而 READ COMMITTED 下只加行锁,且不锁间隙。
- 对一致性要求不苛刻的报表类查询,可设为
READ COMMITTED(需确认 binlog 格式为ROW,否则主从可能不一致) - 避免在
WHERE条件中使用非索引字段触发全表扫描锁——哪怕只是SELECT ... FOR UPDATE,也会锁住所有扫描过的行甚至间隙 - 更新语句尽量走覆盖索引:如果
UPDATE t SET status=1 WHERE user_id=123中user_id是二级索引,InnoDB 会先锁二级索引记录,再锁聚簇索引记录;若该二级索引非唯一,还可能锁住相邻键之间的间隙
减少锁竞争:批量操作与拆分更新
单条 UPDATE 按主键更新很快,但批量更新(如 UPDATE t SET x=y WHERE id IN (1,2,3,...1000))可能一次性锁住大量记录,阻塞其他请求。
- 将大批量更新拆成每批 100–500 行,用
LIMIT+WHERE id > ?分页执行,并在每次提交后短暂休眠(如 10ms),缓解锁压力 - 用
INSERT ... ON DUPLICATE KEY UPDATE替代“先查后更”逻辑,避免 SELECT 加的共享锁与后续 UPDATE 的排他锁之间产生竞争 - 对高频更新的计数器类字段(如阅读量),考虑用 redis 缓存 + 定时落库,绕过 InnoDB 行锁
识别并规避隐式锁升级与元数据锁(MDL)
有些操作看似不涉及数据修改,却会持锁很久,比如 ALTER TABLE 或未加 WHERE 的 delete,它们可能触发表级元数据锁(MDL),阻塞所有 DML。
-
SELECT不加锁,但若执行期间有并发ALTER TABLE,会因 MDL 等待而卡住——可通过performance_schema.metadata_locks查看锁持有者 - 避免在业务高峰期执行 DDL;对大表改结构,优先用
pt-online-schema-change或 MySQL 8.0+ 的ALGORITHM=INSTANT/ALGORITHM=INPLACE - 注意存储过程或触发器中隐式开启事务:哪怕只有一条
INSERT,也会让整个调用过程处于事务中,延长锁生命周期
锁优化的本质不是“怎么加得更少”,而是“让锁的时间更短、范围更准、冲突更可预测”。很多线上锁问题,根源不在 SQL 写法,而在事务边界模糊、索引缺失、或把数据库当缓存用。