연결 복원력

연결 복원력은 실패한 데이터베이스 명령을 자동으로 다시 시도합니다. 이 기능은 실패를 감지하고 명령을 다시 시도하는 데 필요한 논리를 캡슐화하는 "실행 전략"을 제공하여 모든 데이터베이스에서 사용할 수 있습니다. EF Core 공급자는 특정 데이터베이스 오류 조건과 최적의 다시 시도 정책에 맞는 실행 전략을 제공할 수 있습니다.

예를 들어 SQL Server 공급자에는 SQL Server(SQL Azure 포함)에 맞게 특별히 조정된 실행 전략이 포함되어 있습니다. 실행 전략은 다시 시도할 수 있는 예외 유형을 인식하고 최대 다시 시도 횟수, 다시 시도 간 지연 등에 대한 합리적인 기본값을 가지고 있습니다.

실행 전략은 컨텍스트에 대한 옵션을 구성할 때 지정됩니다. 이는 일반적으로 파생 컨텍스트의 OnConfiguring 메서드에 있습니다.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True",
            options => options.EnableRetryOnFailure());
}

또는 ASP.NET Core 애플리케이션의 경우 Startup.cs에 있습니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

참고 항목

실패 시 다시 시도를 사용하도록 설정하면 EF가 결과 집합을 내부적으로 버퍼링하므로 큰 결과 집합을 반환하는 쿼리에 대한 메모리 요구 사항이 크게 늘어날 수 있습니다. 자세한 내용은 버퍼링 및 스트리밍을 참조하세요.

사용자 지정 실행 전략

기본값을 변경하려는 경우 사용자 고유의 사용자 지정 실행 전략을 등록하는 메커니즘이 있습니다.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

실행 전략 및 트랜잭션

실패 시 자동으로 다시 시도하는 실행 전략은 실패한 다시 시도 블록에서 각 작업을 재생할 수 있어야 합니다. 다시 시도가 설정되면, EF 코어를 통해 수행하는 각 작업은 자체적으로 다시 시도할 수 있는 작업이 됩니다. 즉, 일시적인 오류가 발생할 경우 SaveChanges()에 대한 각 쿼리와 각 호출이 하나의 단위로 다시 시도됩니다.

그러나 코드가 BeginTransaction()을 사용하여 트랜잭션을 시작한 경우, 하나의 단위로 처리해야 하는 고유한 작업 그룹을 정의하고, 오류가 발생하면 트랜잭션 내부의 모든 항목이 다시 재생되어야 합니다. 실행 전략을 사용할 때 이 작업을 수행하려고 하면 다음과 같은 예외가 표시됩니다.

InvalidOperationException: 구성된 실행 전략 'SqlServerRetryingExecutionStrategy’는 사용자가 시작한 트랜잭션을 지원하지 않습니다. 'DbContext.Database.CreateExecutionStrategy()'에서 반환된 실행 전략을 사용하여 트랜잭션을 다시 시도가 가능한 단위로 트랜잭션의 모든 작업을 실행합니다.

해결 방법은 실행해야 하는 모든 것을 나타내는 대리자를 사용하여 실행 전략을 수동으로 호출하는 것입니다. 일시적인 오류가 발생하면, 실행 전략에서 대리자를 다시 호출합니다.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context = new BloggingContext();
        using var transaction = context.Database.BeginTransaction();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        context.SaveChanges();

        transaction.Commit();
    });

이 방법은 주변 트랜잭션과 함께 사용할 수도 있습니다.


using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

var strategy = context1.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context2.SaveChanges();

        context1.SaveChanges();

        transaction.Complete();
    });

트랜잭션 커밋 실패 및 멱등성 문제

일반적으로 연결 오류가 발생하면 현재 트랜잭션이 롤백됩니다. 그러나 트랜잭션이 커밋되는 동안 연결이 끊어지면 트랜잭션의 결과 상태를 알 수 없습니다.

기본적으로 실행 전략은 트랜잭션이 롤백된 것처럼 작업을 다시 시도하지만, 그렇지 않은 경우에는 새 데이터베이스 상태가 호환되지 않을 때 예외가 발생하거나, 작업이 특정 상태에 의존하지 않을 때(예: 자동 생성된 키 값이 새 행을 삽입하는 경우) 데이터 손상을 초래할 수 있습니다.

이 문제를 처리하는 방법에는 여러 가지가 있습니다.

옵션 1 - (거의) 아무 작업도 수행하지 않음

트랜잭션 커밋 중에 연결이 실패할 가능성은 낮으므로 이 조건이 실제로 발생하면 애플리케이션이 실패하는 것이 허용될 수 있습니다.

그러나 중복 행을 추가하는 대신 예외가 throw되도록 하려면 저장소 생성 키를 사용하지 않아야 합니다. 클라이언트에서 생성된 GUID 값 또는 클라이언트 측 값 생성기를 사용하는 것이 좋습니다.

옵션 2 - 애플리케이션 상태 다시 빌드

  1. 현재 DbContext를 삭제합니다.
  2. DbContext를 만들고 데이터베이스에서 애플리케이션의 상태를 복원합니다.
  3. 마지막 작업이 성공적으로 완료되지 않았을 수 있음을 사용자에게 알립니다.

옵션 3 - 상태 확인 추가

데이터베이스 상태를 변경하는 대부분의 작업에 대해 성공 여부를 확인하는 코드를 추가할 수 있습니다. EF는 이 작업을 더 쉽게 수행할 수 있는 확장 메서드인 IExecutionStrategy.ExecuteInTransaction를 제공합니다.

이 메서드는 트랜잭션을 시작하고 커밋하며 트랜잭션 커밋 중에 일시적인 오류가 발생할 때 호출되는 verifySucceeded 매개 변수의 함수도 허용합니다.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

db.ChangeTracker.AcceptAllChanges();

참고 항목

이때 SaveChangesSaveChanges가 성공하는 경우 Blog 엔터티 상태가 Unchanged로 변경되는 것을 방지하기 위해 acceptAllChangesOnSuccessfalse로 설정하여 호출됩니다. 이렇게 하면 커밋이 실패하고 트랜잭션이 롤백되는 경우 동일한 작업을 다시 시도할 수 있습니다.

옵션 4 - 수동으로 트랜잭션 추적

저장소 생성 키를 사용해야 하거나 수행된 작업에 의존하지 않는 커밋 실패를 처리하는 일반적인 방법이 필요한 경우 각 트랜잭션에서 커밋이 실패할 때 확인되는 ID를 할당할 수 있습니다.

  1. 트랜잭션 상태를 추적하는 데 사용되는 데이터베이스에 테이블을 추가합니다.
  2. 각 트랜잭션의 시작 부분에 있는 테이블에 행을 삽입합니다.
  3. 커밋 중에 연결이 실패하면 데이터베이스에 해당 행이 있는지 확인합니다.
  4. 커밋에 성공하면 테이블이 증가하지 않도록 해당 행을 삭제합니다.

using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
db.SaveChanges();

참고 항목

확인에 사용된 컨텍스트에 트랜잭션 커밋 중에 연결이 실패한 경우 확인 중에 연결이 다시 실패할 수 있으므로 정의된 실행 전략이 있는지 확인합니다.

추가 리소스