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

EF Core并发表更新问题:寻求无触发器且避免死锁的方案

并发场景下ReadyProduct数据更新冲突的解决方案

当前代码在并发调用时,会出现读取旧数据或更新冲突的问题:

// 并发调用时,有时会读取到旧数据或出现更新冲突
var readyProduct = await _readyProductRepository.FindAsync(p => p.ModelId == readyProductCreate.ModelId);

if (readyProduct == null)
{
    readyProduct = _mapper.Map<ReadyProductCreate, ReadyProduct>(readyProductCreate);

    await _readyProductRepository.AddAsync(readyProduct);
}
else
{
    readyProduct.Count += readyProductCreate.Count;
    _readyProductRepository.Update(readyProduct);
}

var transaction = new ReadyProductTransaction
{
    ModelId = readyProductCreate.ModelId,
    Count = readyProductCreate.Count,
    Status = ReadyProductTransactionType.Import,
    Date = DateTime.Now,
};

await _transactionRepository.AddAsync(transaction);

await _unitOfWork.SaveAsync();

之前尝试用Serializable隔离级事务包裹整个流程,但引发了死锁。考虑过通过ReadyProductTransaction表的插入触发器更新ReadyProduct表,但希望找到无需触发器的解决方案。

环境信息:

  • DbContext注册为Scoped
  • 包括UnitOfWork在内的所有仓储类均注册为Scoped
  • 应用基于.NET 5.0
  • 部署在IIS上

可行解决方案

1. 乐观锁(推荐)

给ReadyProduct实体加一个RowVersion字段,对应数据库的timestamp/rowversion类型,EF Core会自动检测并发冲突,冲突时重试即可:

首先修改实体类:

public class ReadyProduct
{
    // 原有字段保留
    public int Count { get; set; }
    // 添加行版本字段
    public byte[] RowVersion { get; set; }
}

在DbContext配置中映射该字段:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ReadyProduct>()
        .Property(p => p.RowVersion)
        .IsRowVersion();
}

然后修改业务逻辑,捕获并发异常并重试:

// 设置重试次数,可根据业务调整
int retryTimes = 3;
while (retryTimes-- > 0)
{
    try
    {
        var readyProduct = await _readyProductRepository.FindAsync(p => p.ModelId == readyProductCreate.ModelId);
        if (readyProduct == null)
        {
            readyProduct = _mapper.Map<ReadyProductCreate, ReadyProduct>(readyProductCreate);
            await _readyProductRepository.AddAsync(readyProduct);
        }
        else
        {
            readyProduct.Count += readyProductCreate.Count;
            _readyProductRepository.Update(readyProduct);
        }

        var transaction = new ReadyProductTransaction
        {
            ModelId = readyProductCreate.ModelId,
            Count = readyProductCreate.Count,
            Status = ReadyProductTransactionType.Import,
            Date = DateTime.Now,
        };
        await _transactionRepository.AddAsync(transaction);

        await _unitOfWork.SaveAsync();
        // 成功就跳出循环
        break;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // 刷新实体的最新数据,准备重试
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is ReadyProduct product)
            {
                var dbValues = await entry.GetDatabaseValuesAsync();
                if (dbValues != null)
                {
                    entry.CurrentValues.SetValues(dbValues);
                }
                else
                {
                    // 实体已被删除,根据业务需求处理,这里直接抛出
                    throw new InvalidOperationException("目标商品已被删除,无法完成操作", ex);
                }
            }
            else
            {
                throw new NotSupportedException("无法处理该类型的并发冲突", ex);
            }
        }
    }
}

乐观锁不会长期占用锁资源,适合高并发场景,只有冲突发生时才重试,性能影响小。

2. 行级悲观锁

通过EF Core执行带锁的SQL查询,锁定目标行,避免其他事务修改:

// 以SQL Server为例,用UPDLOCK+HOLDLOCK锁定行
var readyProduct = await _context.ReadyProducts
    .FromSqlRaw("SELECT * FROM ReadyProducts WITH (UPDLOCK, HOLDLOCK) WHERE ModelId = {0}", readyProductCreate.ModelId)
    .FirstOrDefaultAsync();

// 后续逻辑和原代码一致
if (readyProduct == null)
{
    readyProduct = _mapper.Map<ReadyProductCreate, ReadyProduct>(readyProductCreate);
    await _readyProductRepository.AddAsync(readyProduct);
}
else
{
    readyProduct.Count += readyProductCreate.Count;
    _readyProductRepository.Update(readyProduct);
}

var transaction = new ReadyProductTransaction
{
    ModelId = readyProductCreate.ModelId,
    Count = readyProductCreate.Count,
    Status = ReadyProductTransactionType.Import,
    Date = DateTime.Now,
};
await _transactionRepository.AddAsync(transaction);

await _unitOfWork.SaveAsync();
  • UPDLOCK:获取更新锁,允许其他事务读取但禁止修改
  • HOLDLOCK:保持锁直到事务结束
    这种方式能确保当前事务拿到最新数据并独占修改权,相比Serializable隔离级,只锁定单行,死锁概率低很多,但要注意控制事务时长,避免锁持有过久影响性能。

3. 原子UPSERT操作

直接用SQL执行原子的插入/更新(UPSERT),把判断和更新放在数据库层面完成,彻底避免先读后写的竞争:

// SQL Server环境下的UPSERT语句,其他数据库可调整语法
var upsertSql = @"
IF EXISTS (SELECT 1 FROM ReadyProducts WHERE ModelId = @ModelId)
    UPDATE ReadyProducts SET Count = Count + @Count WHERE ModelId = @ModelId
ELSE
    INSERT INTO ReadyProducts (ModelId, Count /* 其他必填字段 */) 
    VALUES (@ModelId, @Count /* 对应字段值 */)";

// 执行原子更新
await _context.Database.ExecuteSqlRawAsync(upsertSql, 
    new SqlParameter("@ModelId", readyProductCreate.ModelId),
    new SqlParameter("@Count", readyProductCreate.Count));

// 插入交易记录
var transaction = new ReadyProductTransaction
{
    ModelId = readyProductCreate.ModelId,
    Count = readyProductCreate.Count,
    Status = ReadyProductTransactionType.Import,
    Date = DateTime.Now,
};
await _transactionRepository.AddAsync(transaction);

await _unitOfWork.SaveAsync();

这种方式性能最优,完全避免并发问题,但需要手动编写SQL,灵活性稍差,适合业务逻辑固定的场景。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 12:22:17