我有关于SaveChangesAsync()
和 BeginTransaction()
+ transaction.Commit()
的问题。
我们团队有一个.NET Core worker,通过EF Core 3从Microsoft EventHub接收事件并将数据保存到SQL Server中。
其中一个事件类型有很多数据,因此我们创建了几个表,将数据分离后再保存到这些表中,子表引用了父表的id
列(FK_Key)。
在某些条件下,在保存新数据之前必须删除数据库中的一些数据,因此我们进行删除->更新操作。
为了将数据保存到数据库中,我们调用dbContext.Database.BeginTransaction()
和transaction.Commit()
。当我们运行worker时,会出现死锁异常,如Transaction (Process ID 71) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
我发现PurgeDataInChildTables()
中的.BatchDeleteAsync()
或Upsert()
中的BulkInsertOrUpdateAsync()
之一会抛出死锁异常(每次运行worker时都会更改)。
以下是代码:
public async Task DeleteAndUpsert(List<MyEntity> entitiesToDelete, List<MyEntity> entitiesToUpsert)
{
if (entitiesToDelete.Any())
await myRepository.Delete(entitiesToDelete);
if (entitiesToUpsert.Any())
await myRepository.Upsert(entitiesToUpsert);
}
public override async Task Upsert(IList<MyEntity> entities)
{
using (var dbContext = new MyDbContext(DbContextOptions, DbOptions))
{
using (var transaction = dbContext.Database.BeginTransaction())
{
await PurgeDataInChildTables(entities, dbContext);
await dbContext.BulkInsertOrUpdateAsync(entities);
// tables that depends on the parent table (FK_Key)
await dbContext.BulkInsertOrUpdateAsync(entities.SelectMany<Child1>(x => x.Id).ToList());
await dbContext.BulkInsertOrUpdateAsync(entities.SelectMany<Child2>(x => x.Id).ToList());
await dbContext.BulkInsertOrUpdateAsync(entities.SelectMany<Child3>(x => x.Id).ToList());
transaction.Commit();
}
}
}
public override async Task Delete(IList<MyEntity> entities)
{
using (var dbContext = new MyDbContext(DbContextOptions, DbOptions))
{
using (var transaction = dbContext.Database.BeginTransaction())
{
await PurgeDataInChildTables(entities, dbContext);
await dbContext.BulkDeleteAsync(entities);
transaction.Commit();
}
}
}
private async Task PurgeDataInChildTables(IList<MyEntity> entities, MyDbContext dbContext)
{
var ids = entities.Select(x => x.Id).ToList();
await dbContext.Child1.Where(x => ids.Contains(x.Id)).BatchDeleteAsync();
await dbContext.Child2.Where(x => ids.Contains(x.Id)).BatchDeleteAsync();
await dbContext.Child3.Where(x => ids.Contains(x.Id)).BatchDeleteAsync();
}
当 worker 启动时,它会创建四个线程,它们都会对同一个表进行 upsert 操作(也会删除)。因此,我认为当一个线程开始事务,另一个线程开始另一个事务(或类似情况),然后尝试对子表进行 upsert 操作(或从中删除)时,就会发生死锁。
我尝试了一些方法来解决这个问题,并注意到当我删除 BeginTransaction()
并改用 SaveChangesAsync()
时,死锁似乎得到了解决。
以下是修改后的代码:
public override async Task Upsert(IList<MyEntity> entities)
{
using (var dbContext = new MyDbContext(DbContextOptions, DbOptions))
{
await PurgeDataInChildTables(entities, dbContext);
await dbContext.BulkInsertOrUpdateAsync(entities);
// tables that depends on the parent table (FK_Key)
await dbContext.BulkInsertOrUpdateAsync(entities.SelectMany(x => x.Child1).ToList());
await dbContext.BulkInsertOrUpdateAsync(entities.SelectMany(x => x.Child2).ToList());
await dbContext.BulkInsertOrUpdateAsync(entities.SelectMany(x => x.Child3).ToList());
await dbContext.SaveChangesAsync();
}
}
public override async Task Delete(IList<MyEntity> entities)
{
using (var dbContext = new MyDbContext(DbContextOptions, DbOptions))
{
await PurgeDataInChildTables(entities, dbContext);
await dbContext.BulkDeleteAsync(entities);
await dbContext.SaveChangesAsync();
}
}
在工作程序启动后约30秒钟,死锁问题会发生,但是当我修改了代码后,2-3分钟内没有发生,所以我认为问题已经解决了,不过如果我让worker运行更长的时间,可能仍然会出现问题。
最后,以下是我的问题:
- 当我使用
BeginTransaction()
+.Commit()
时,会发生死锁,但当我使用SaveChangesAsync()
时却不会。为什么? - 这两种方法在事务方面有什么区别?
- 如果修改后的代码仍可能导致死锁或不是一个好的解决方案,我该如何解决?
BeginTransaction()
,因为我想在单个过程中更新/删除多个表中的行。我应该将.Select(x => ....
移到事务之外,然后可能更改隔离级别以适当的方式。 - KojiSQL Server Management Studio
来检查是否有等待的事务,只需查看活动进程即可 - 它们将处于空闲状态。 - cassandradlock
。这将确保只有一个线程可以同时进行读取/更新/删除操作,但我试图避免使用它,因为它会显著减慢线程的速度。 - KojiROWLOCK
优化提示,但请注意,在某些情况下,它可能会导致性能下降。因此,在这里手动锁定看起来是一个很好的解决方案,因为无论在哪一侧采取,无论是 C# 代码还是数据库,锁定都将被采取。 - cassandrad