Есть ли у вас опыт работы с транзакциями в .NET и базах данных?

«Есть ли у вас опыт работы с транзакциями в .NET и базах данных?» — вопрос из категории Базы данных, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, есть глубокий опыт работы с транзакциями на разных уровнях стека: от транзакций в одной базе данных до распределенных сценариев.

1. Транзакции в рамках одной базы данных (ADO.NET / Dapper):

  • Классический подход с SqlTransaction:

    using (var connection = new SqlConnection(connectionString))
    {
        await connection.OpenAsync();
        using (var transaction = await connection.BeginTransactionAsync())
        {
            try
            {
                await connection.ExecuteAsync(
                    "UPDATE Accounts SET Balance = Balance - @Amount WHERE Id = @FromId",
                    new { Amount = 100, FromId = 1 }, transaction);
    
                await connection.ExecuteAsync(
                    "UPDATE Accounts SET Balance = Balance + @Amount WHERE Id = @ToId",
                    new { Amount = 100, ToId = 2 }, transaction);
    
                await transaction.CommitAsync(); // Фиксация только здесь
            }
            catch
            {
                await transaction.RollbackAsync(); // Откат при ошибке
                throw;
            }
        }
    }

2. Упрощенный подход с TransactionScope:

  • Позволяет создавать неявные транзакции, которые автоматически повышаются до распределенных (DTC) при использовании нескольких соединений.

    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        // Операция 1 (например, с SQL Server)
        await _repo1.UpdateDataAsync();
        // Операция 2 (например, с другой БД или RabbitMQ через DTC-совместимый ресурс)
        await _repo2.LogOperationAsync(); 
    
        scope.Complete(); // Отметка для коммита
    } // Если Complete() не вызван, произойдет откат

3. Уровни изоляции (IsolationLevel): Критически важный параметр, влияющий на параллелизм и корректность данных. Чаще всего используются:

  • ReadCommitted (по умолчанию в SQL Server): Защищает от "грязного" чтения. Баланс между согласованностью и производительностью.
  • RepeatableRead: Гарантирует, что строки, прочитанные в транзакции, не будут изменены другими транзакциями.
  • Serializable: Самый строгий уровень, эмулирует последовательное выполнение транзакций. Может приводить к блокировкам и снижению производительности.
  • Snapshot / ReadCommittedSnapshot: Использует управление параллелизмом на основе версий строк (MVCC), что уменьшает блокировки. Требует включения на уровне БД.

4. Распределенные транзакции и современные подходы:

  • DTC (Distributed Transaction Coordinator): Классическое, но тяжелое решение для распределенных транзакций. В облачных и микросервисных средах его использование часто не рекомендуется из-за сложности и проблем с производительностью/доступностью.
  • Альтернативы в микросервисах: Для согласованности данных между сервисами используются паттерны Saga (компенсирующие транзакции) или Outbox Pattern (для гарантированной доставки событий в рамках локальной транзакции БД).

5. ORM (Entity Framework Core):

  • EF Core автоматически управляет транзакцией при вызове SaveChangesAsync(). Для явного управления можно использовать IDbContextTransaction.
    await using var transaction = await _context.Database.BeginTransactionAsync();
    try
    {
        _context.Orders.Add(newOrder);
        _context.InventoryItems.Update(item);
        await _context.SaveChangesAsync(); // Операции выполняются в одной транзакции
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }

Опыт включает настройку таймаутов, обработку дедлоков, выбор оптимального уровня изоляции под конкретную нагрузку и отказ от DTC в пользу идемпотентных, компенсируемых операций в распределенных системах.