ER_LOCK_DEADLOCK主因是事务过长而非锁未释放:大事务导致锁持有久、范围宽(如行锁升级间隙锁),引发循环等待被InnoDB检测并回滚。应按主键分片拆分为多个小事务,显式控制边界,避免批量操作与混合DML+DDL。

为什么ER_LOCK_DEADLOCK不是锁没释放,而是事务太长
MySQL报ER_LOCK_DEADLOCK,不代表你代码漏了COMMIT或有长连接占着锁不放——绝大多数情况是单个事务里干了太多事:批量更新几百行、边查边改再插日志、嵌套循环里反复UPDATE。InnoDB在加锁过程中发现循环等待,直接挑一个事务回滚,抛出这个错误。
死锁检测本身很快,但大事务会让锁持有时间变长、锁范围变宽(比如从行锁升级为间隙锁),冲突概率指数级上升。别急着调innodb_lock_wait_timeout,那只是让等更久,不是解决问题。
- 典型场景:
UPDATE带子查询、INSERT ... SELECT扫描全表、事务内混合DML+DDL - 容易被忽略的点:ORM里一个
.save()可能隐含SELECT FOR UPDATE+UPDATE两步锁 - 用
SHOW ENGINE INNODB STATUS\G看LATEST DETECTED DEADLOCK段,重点盯TRANSACTION下的lock_mode和lock_trx_id,确认是不是同一个事务反复申请锁
怎么拆?按主键ID分片+显式控制事务边界
不能靠“尽量少写SQL”这种模糊建议。得把一个大事务切分成多个小事务,每个只操作一批主键连续、数量可控的记录,并确保每次提交后锁立刻释放。
示例:原逻辑是UPDATE orders SET status = 'shipped' WHERE created_at ,影响5万行。
SELECT id FROM orders WHERE created_at < '2024-01-01' ORDER BY id LIMIT 1000 OFFSET 0;
拿到这批id后,用IN精准更新,包在独立事务里:
BEGIN; UPDATE orders SET status = 'shipped' WHERE id IN (1001,1002,...,2000); COMMIT;
- 每次最多处理100–1000行,具体看单行数据大小和索引效率;超1000行容易触发自增锁或间隙锁扩大
- 必须按
PRIMARY KEY或有索引的字段排序分片,否则OFFSET会越来越慢 - 避免用
WHERE created_at < ...直接更新,条件字段无索引时会全表扫描加锁,极易死锁
Python/Java里怎么安全实现分片更新
手动写WHILE循环分页容易漏掉最后一批或重复处理。关键不是语言特性,而是保证“取一批、更一批、记位点、不跳过”。
Python示例(用pymysql):
last_id = 0
while True:
cursor.execute(
"SELECT id FROM orders WHERE id > %s AND created_at < %s ORDER BY id LIMIT 1000",
(last_id, '2024-01-01')
)
rows = cursor.fetchall()
if not rows:
break
ids = [r[0] for r in rows]
# 新事务
conn.begin()
cursor.execute(
"UPDATE orders SET status = 'shipped' WHERE id IN (" + ",".join(["%s"] * len(ids)) + ")",
ids
)
conn.commit()
last_id = ids[-1]- 不用
OFFSET,改用WHERE id > last_id,避免深度分页性能坍塌 - 每批
UPDATE前必须conn.begin(),不能复用上一个事务的连接状态 - Java里用
JdbcTemplate.batchUpdate()也得配TransactionTemplate,否则默认走全局事务 - 注意时区:如果
created_at是DATETIME且应用与DB时区不一致,条件可能漏数据
哪些操作看似安全,其实照样引发ER_LOCK_DEADLOCK
很多人以为“我只是读,不写,肯定不锁”,或者“我用了SELECT ... FOR UPDATE但只查一行,没问题”——错。锁的粒度和时机远比想象中复杂。
-
SELECT ... FOR UPDATE在可重复读隔离级别下,会对查询范围加间隙锁(gap lock),哪怕只查id = 100,也可能锁住(90,110)这段空隙 -
INSERT INTO ... SELECT语句,SELECT部分会按执行计划加锁,如果没走索引,等于给全表上读锁,接着INSERT又要加写锁,死锁温床 - 外键约束触发的隐式
SELECT:比如删父表记录,InnoDB会自动检查子表,若子表没索引,就全表扫描加锁 - 唯一索引冲突时的插入重试:
INSERT IGNORE或ON DUPLICATE KEY UPDATE在冲突时会先加S锁再转X锁,高并发下易形成锁等待链
真正难处理的,是那些你以为“只读”“只插一行”“肯定走索引”的操作,在数据分布偏斜、统计信息过期、或执行计划突变时突然开始锁表。监控innodb_row_lock_waits比等报错更有意义。


















