update锁表时间长的根本原因是事务未及时结束,导致行锁升级为间隙锁、临键锁甚至全表锁;实操需分批更新、加索引、简化事务、降隔离级别、慎用子查询、规范事务边界、处理死锁并监控长事务。

UPDATE 语句锁表时间长,根本原因是事务没及时结束
MySQL(尤其是 InnoDB)的 UPDATE 默认走行锁,但一旦事务不提交、或扫描范围过大、或缺少索引,就会升级为间隙锁、临键锁,甚至锁整张表。锁表时间 ≠ SQL 执行时间,而是从 BEGIN 到 COMMIT 或 ROLLBACK 的整个窗口期。
实操建议:
- 把
UPDATE拆成小批量执行,比如每次只更新1000行,用LIMIT控制(注意:MySQL 5.7+ 支持LIMIT在UPDATE中使用) - 确保
WHERE条件字段有有效索引,否则会触发全表扫描 → 全表加锁 - 避免在事务里混杂 SELECT + UPDATE + 外部 API 调用,网络延迟会把锁拖得更久
- 确认隔离级别:如果业务允许,把
REPEATABLE READ降为READ COMMITTED,能减少间隙锁范围
批量 UPDATE 时用 JOIN 还是子查询?性能与锁行为差异大
用 UPDATE ... JOIN 和 UPDATE ... WHERE id IN (SELECT ...) 看似等价,但锁机制完全不同。前者通常只锁被更新的行及其关联行;后者在 MySQL 5.7 及以前可能对子查询结果集所有行加锁(即使最终没更新),且容易触发临时表和全表扫描。
实操建议:
- 优先用
UPDATE t1 JOIN t2 ON t1.id = t2.t1_id SET t1.status = t2.new_status,明确控制锁范围 - 避免
UPDATE ... WHERE id IN (SELECT id FROM large_table WHERE ...),改用 JOIN 或分批主键列表 - 如果必须用子查询,确保子查询能命中索引,并加上
FORCE INDEX提示(如SELECT id FROM t FORCE INDEX (idx_status) WHERE status = 'pending')
事务边界模糊导致“隐式长事务”,DBA 都难定位
常见现象:应用层没显式开事务,但 ORM(如 Django、Spring)自动开启了事务;或框架配置了 @Transactional 却套在耗时操作(如文件处理、HTTP 请求)外面;又或者连接池复用后,上一个请求没 commit,下一个请求接着用同一个连接 —— 锁就一直挂着。
实操建议:
- 查
SHOW ENGINE INNODB STATUS里的TRANSACTIONS部分,重点关注trx_started和trx_mysql_thread_id,结合SHOW PROCESSLIST定位长事务线程 - Django 中检查是否误用了
atomic装饰器包裹了非 DB 操作;Spring 中确认propagation是否设为REQUIRED导致意外嵌套 - 在数据库侧设置超时:MySQL 可配
innodb_lock_wait_timeout=10(秒),并配合应用层捕获Lock wait timeout exceeded错误做重试或告警
并发 UPDATE 同一行时死锁不是 bug,是设计必然
两个事务同时想更新同一行,各自先持有自己的锁再申请对方的锁,就构成循环等待。MySQL 会主动选一个事务回滚(报错 Deadlock found when trying to get lock),这不是异常,而是并发控制的正常代价。
实操建议:
- 让所有业务按固定顺序更新多行(比如总是先更新 user 表再更新 order 表),从源头减少循环依赖
- 应用层捕获
Deadlock found when trying to get lock,做指数退避重试(最多 3 次),不要静默失败 - 避免在事务中 SELECT FOR UPDATE 后隔很久才 UPDATE,中间空窗期极易引发死锁竞争
- 用
EXPLAIN UPDATE ...(MySQL 8.0.19+ 支持)看执行计划,确认是否走了预期索引,避免锁范围扩大
真正卡住系统的,往往不是单条慢 SQL,而是那个忘了 commit 的事务、那个没索引的 WHERE 条件、或者那个在事务里调了三次微信接口的函数——它们不会报错,但会让锁在后台无声蔓延。










