在日常的数据库开发中,很多同学会遇到这样的场景:
-
某个操作用了
BEGIN ... COMMIT显式事务; -
另一个操作只是单条
UPDATE,没有显式事务; -
两边同时执行,却出现了 死锁(Deadlock) 报错。
这让人很困惑:明明只有一边用了事务,为什么还会死锁呢?
一、事务与锁的关系
首先要明确一点:
👉 所有的更新操作本质上都运行在事务里。
-
如果你写了
BEGIN ... COMMIT,这是显式事务。 -
如果你没有写事务,MySQL 会自动给每条语句包裹一个隐式事务,执行完立即提交。
也就是说,就算你没有手动开事务,UPDATE 也会获取行锁,只不过锁的持有时间很短(执行完马上释放)。
二、死锁是如何产生的?
死锁的条件是:
-
至少有两个事务;
-
事务 A 占有资源 1 等待资源 2;
-
事务 B 占有资源 2 等待资源 1;
-
最终形成环路,谁也不释放。
举个例子:
-- Session A(显式事务)
BEGIN;
UPDATE product_audit SET edit_record = 'xxx' WHERE id = 1;
UPDATE product_audit SET edit_record = 'yyy' WHERE id = 2;
-- A 已锁定 id=1,正在等待 id=2
-- Session B(隐式事务)
UPDATE product_audit SET edit_record = 'zzz' WHERE id = 2;
UPDATE product_audit SET edit_record = 'aaa' WHERE id = 1;
-- B 已锁定 id=2,正在等待 id=1
此时,A 等待 B,B 等待 A,死锁就产生了。
三、为什么“一个加事务,一个不加事务”更容易死锁?
原因在于锁的持有时间不同:
-
显式事务:锁会持续到
COMMIT/ROLLBACK,时间长。 -
隐式事务:锁在单条语句执行完就释放,时间短。
当两者并发执行时,显式事务持有锁不释放,隐式事务正好卡在等待中,就可能形成死锁。
换句话说,并不是“只有都加事务才会死锁”,只要两个事务更新了相同或相邻的行,死锁就有可能发生。
四、避免死锁的几种方法
-
控制批量更新大小
不要一次更新太多行,使用分批UPDATE,例如array_chunk拆分。 -
保持更新顺序一致
保证所有事务按相同顺序更新数据,比如都按 ID 升序更新。 -
捕获死锁并重试
在 Laravel/PHP 中,可以使用事务包装,设置重试机制: -
检查索引
确认WHERE条件能走索引,否则会锁住更多的行,放大死锁风险。
五、总结
-
有无显式事务,并不能决定是否会死锁;
-
所有更新操作都运行在事务里,区别只是锁的持有时间长短;
-
死锁的根源是不同事务加锁顺序不同,因此要么控制更新顺序,要么减少锁范围。
在高并发环境下,建议始终考虑分批更新、加索引、重试机制,才能有效降低死锁带来的影响。
2025-09-11 17:19:32,若文章内容或图片失效,请留言或联系站长反馈!










暂无评论内容