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 <p>拿到这批<code>id</code>后,用<code>IN</code>精准更新,包在独立事务里:</p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill2334" title="MySQL"><img src="https://img.php.cn/upload/skill/000/000/081/178900927846657.jpg" alt="MySQL" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/skill2334" title="MySQL" class="overflowclass">MySQL</a> <p class="overflowclass">编写正确的MySQL查询,避免字符集、索引和锁方面的常见陷阱。</p> </div> <a rel="nofollow" href="/xiazai/skill2334" title="MySQL" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div><pre class="brush:php;toolbar:false;">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
- 不用
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比等报错更有意义。










