У меня есть вопросы о том, как работают SaveChangesAsync()
и BeginTransaction()
+ transaction.Commit()
.
В моей команде есть. NET Core worker, который получает события от Microsoft EventHub и сохраняет данные на SQL сервере через EF Core 3.
Один из типов событий содержит много данных, поэтому мы создал несколько таблиц, разделил данные и затем сохранил их в этих таблицах. Дочерние таблицы ссылаются на столбец id
(FK_Key) родительской таблицы.
Некоторые данные в БД должны быть удалены перед сохранением новых данных при определенных условиях, поэтому мы удаляем -> вставляем данные.
Для сохранения данных в БД мы вызываем dbContext.Database.BeginTransaction()
и transaction.Commit()
. Когда мы запускаем воркер, мы получаем исключение взаимоблокировки, например Transaction (Process ID 71) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Я обнаружил, что один из .BatchDeleteAsync()
в PurgeDataInChildTables()
или один из BulkInsertOrUpdateAsync()
в Upsert()
выдает исключение взаимоблокировки (оно меняется каждый раз). раз бегаю рабочий).
Вот код:
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();
}
Когда рабочий запускается, он создает четыре потока, и все они загружаются в одну и ту же таблицу (и удаляются тоже). Итак, я предполагаю, что тупик возникает, когда один поток запускает транзакцию, а другой запускает другую транзакцию (или что-то подобное ..), а затем пытается выполнить вставку (или удаление из) дочерних таблиц.
Я попробовал кое-что, чтобы решить проблему и заметил, что тупик, кажется, разрешен, когда я удаляю 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 минут, когда я изменил код, поэтому Я думаю, что проблема решена, подумал, что она все еще может возникнуть, если я буду запускать воркер дольше.
Наконец, вот мои вопросы:
- Когда я использую
BeginTransaction()
+ .Commit()
, возникает тупик, но этого не происходит, когда я использую SaveChangesAsync()
. Почему? - В чем разница между этими двумя методами с точки зрения транзакции?
- Если измененный код все еще может вызвать тупик или не является хорошим решением, как мне решить эту проблему?