最近访客

事务与死锁:一个加事务,一个不加事务,会发生什么?

在日常的数据库开发中,很多同学会遇到这样的场景:

  • 某个操作用了 BEGIN ... COMMIT 显式事务;

  • 另一个操作只是单条 UPDATE,没有显式事务;

  • 两边同时执行,却出现了 死锁(Deadlock) 报错。

这让人很困惑:明明只有一边用了事务,为什么还会死锁呢?


一、事务与锁的关系

首先要明确一点:
👉 所有的更新操作本质上都运行在事务里。

  • 如果你写了 BEGIN ... COMMIT,这是显式事务

  • 如果你没有写事务,MySQL 会自动给每条语句包裹一个隐式事务,执行完立即提交。

也就是说,就算你没有手动开事务,UPDATE 也会获取行锁,只不过锁的持有时间很短(执行完马上释放)。


二、死锁是如何产生的?

死锁的条件是:

  1. 至少有两个事务;

  2. 事务 A 占有资源 1 等待资源 2;

  3. 事务 B 占有资源 2 等待资源 1;

  4. 最终形成环路,谁也不释放。

举个例子:

-- 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,时间长。

  • 隐式事务:锁在单条语句执行完就释放,时间短。

当两者并发执行时,显式事务持有锁不释放,隐式事务正好卡在等待中,就可能形成死锁。

换句话说,并不是“只有都加事务才会死锁”,只要两个事务更新了相同或相邻的行,死锁就有可能发生。


四、避免死锁的几种方法

  1. 控制批量更新大小
    不要一次更新太多行,使用分批 UPDATE,例如 array_chunk 拆分。

  2. 保持更新顺序一致
    保证所有事务按相同顺序更新数据,比如都按 ID 升序更新。

  3. 捕获死锁并重试
    在 Laravel/PHP 中,可以使用事务包装,设置重试机制:

    DB::transaction(function () use ($ids, $data) {
        foreach (array_chunk($ids, 50) as $chunk) {
            DB::table('product_audit')
                ->whereIn('id', $chunk)
                ->update(['edit_record' => $data]);
        }
    }, 5); // 遇到死锁重试 5 次

  4. 检查索引
    确认 WHERE 条件能走索引,否则会锁住更多的行,放大死锁风险。


五、总结

  • 有无显式事务,并不能决定是否会死锁

  • 所有更新操作都运行在事务里,区别只是锁的持有时间长短;

  • 死锁的根源是不同事务加锁顺序不同,因此要么控制更新顺序,要么减少锁范围。

在高并发环境下,建议始终考虑分批更新、加索引、重试机制,才能有效降低死锁带来的影响。

温馨提示: 本文最后更新于2025-09-11 17:19:32,若文章内容或图片失效,请留言或联系站长反馈!
本站资源均仅供学习和研究使用,请在下载后24小时内删除!
© 版权声明
THE END
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容