You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

依赖注入Scoped生命周期DBContext时如何使用事务解决死锁

问题背景

托管在Azure上的.NET应用运行时抛出如下错误:

Transaction was deadlocked on lock resources with another process and has been chosen as the deadlock victim.(事务在锁资源上与其他进程发生死锁,已被选为死锁牺牲品)
当前应用采用依赖注入(DI)方式以Scoped(作用域)生命周期获取EF Core的DbContext对象,以下针对相关疑问逐一给出解答:


1. DI注入Scoped DbContext场景下的正确事务使用方式

首先纠正一个常见误解:EF Core使用事务完全不需要手动创建DbContext实例,注入的Scoped DbContext原生支持事务操作,按业务场景分两类用法:

  • 隐式事务:EF Core默认会将所有SaveChanges()/SaveChangesAsync()调用包裹在隐式事务中,如果业务操作仅需单次提交即可完成,不需要额外编写事务代码,默认机制即可保证单批次提交的原子性,多余的显式事务代码反而会拉长持锁时间。
  • 显式事务:需要跨多次SaveChanges调用、或需要自定义事务隔离级别的场景,直接在注入的DbContext实例上调用事务API即可,示例代码如下:
// _dbContext为构造函数注入的Scoped生命周期实例
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);
try
{
    // 执行多组数据库操作
    _dbContext.Orders.Add(newOrder);
    await _dbContext.SaveChangesAsync();

    _dbContext.Inventories.Update(targetStock);
    await _dbContext.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

如果需要在当前请求作用域内跨多个服务复用同一个事务,直接将开启的事务实例传递给同作用域下的其他服务即可,所有复用同一个DbContext实例的操作会自动加入当前事务上下文,不需要额外处理。


2. DI已注入DbContext的前提下手动创建DbContext实例是否推荐

绝对不推荐直接手动new DbContext实例,核心原因如下:

  • 手动创建的DbContext脱离DI容器生命周期管理,很容易出现数据库连接泄漏、配置不一致(无法复用DI中注册的拦截器、连接字符串、执行策略等)、Dispose不及时等问题。
  • 手动创建的多份DbContext会各自维护独立的数据库连接和事务上下文,同一个业务操作中的多组逻辑会落到不同的事务里,反而会大幅提升锁冲突和死锁概率,完全违背使用事务规避死锁的初衷。
  • Scoped生命周期的DbContext本身就是为单个请求/业务作用域复用设计的,天然适配工作单元模式,没有任何手动创建实例的必要。

唯一的例外场景:需要执行和当前业务事务完全无关的独立后台操作,这种场景也不应该直接new DbContext,而是通过IServiceScopeFactory创建独立的DI作用域,再从新作用域中获取DbContext实例,示例代码:

using var independentScope = _serviceScopeFactory.CreateScope();
var separateDbContext = independentScope.ServiceProvider.GetRequiredService<AppDbContext>();
// 用该独立上下文执行和当前事务无原子性要求的操作

3. 该死锁错误的可行解决方案

死锁的本质是多个事务互相持有对方需要的锁、形成循环等待,并非单纯开启事务就能解决,按落地优先级从高到低可采用以下方案:

  • 统一多表访问顺序:梳理所有涉及多表读写的业务逻辑,全局固定表的访问顺序,比如所有涉及订单表、库存表的操作都固定先访问订单表再访问库存表,从根源上避免“A事务锁订单等库存、B事务锁库存等订单”的循环等待,这是解决死锁最彻底的手段。
  • 缩短事务持锁时间:禁止在事务范围内编写任何与数据库无关的逻辑,包括调用第三方接口、执行复杂内存计算、等待外部输入等,所有数据校验、参数准备逻辑全部放在事务开启前完成,事务内仅保留必要的数据库读写和提交操作,尽可能缩小锁占用的时间窗口。
  • 降低锁冲突概率:
    • 非必要场景不要使用高于ReadCommitted的事务隔离级别(如RepeatableRead、Serializable),避免不必要的范围锁。
    • 针对Azure SQL数据库,可开启数据库级别的READ_COMMITTED_SNAPSHOT选项,开启后读操作基于行版本快照执行,不再占用共享锁,从根源上避免读写操作互相阻塞导致的死锁,开启命令如下:
      ALTER DATABASE 你的业务数据库名 SET READ_COMMITTED_SNAPSHOT ON;
      
    • 不要随意给查询加更新锁、排他锁提示,避免人为扩大锁范围。
  • 添加死锁重试机制:利用EF Core内置的执行策略,针对SQL Server/Azure SQL的死锁错误码(1205)配置自动重试,示例配置如下:
services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString, sqlOpts =>
    {
        sqlOpts.EnableRetryOnFailure(
            maxRetryCount: 3,
            maxRetryDelay: TimeSpan.FromMilliseconds(300),
            errorNumbersToAdd: new[] { 1205 }
        );
    });
});

注意重试逻辑对应的业务操作必须保证幂等,避免重试导致数据重复写入。

  • 优化索引缩小锁范围:通过Azure SQL的扩展事件、查询性能洞察工具抓取死锁日志,定位死锁涉及的SQL语句,给查询条件、关联字段添加合适的索引,避免无索引导致的全表扫描,将锁的粒度从整表缩小到目标行,大幅降低锁冲突概率。
  • 错峰执行长耗时操作:报表查询、批量数据更新这类长耗时、锁范围大的操作,统一安排到业务低峰期执行,且批量操作拆分为小批次提交,避免一次性锁定大量数据阻塞线上业务。

内容的提问来源于stack exchange,提问作者Ghanshyam Mirani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 16:33:38