다음을 통해 공유


연결 복원력

연결 복원력은 실패한 데이터베이스 명령을 자동으로 다시 시도합니다. 이 기능은 오류를 감지하고 명령을 다시 시도하는 데 필요한 논리를 캡슐화하는 "실행 전략"을 제공하여 모든 데이터베이스와 함께 사용할 수 있습니다. 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;ConnectRetryCount=0",
            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 Core를 통해 수행하는 각 작업은 자체적으로 다시 시도 가능한 작업이 됩니다. 즉, 일시적인 오류가 발생하면 각 쿼리와 SaveChangesAsync() 대한 각 호출이 하나의 단위로 다시 시도됩니다.

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

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

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


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

await strategy.ExecuteAsync(
    async () =>
    {
        using var context = new BloggingContext();
        await using var transaction = await context.Database.BeginTransactionAsync();

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

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

        await transaction.CommitAsync();
    });

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


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

var strategy = context1.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(
    async () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

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

        await context1.SaveChangesAsync();

        transaction.Complete();
    });

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

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

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

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

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

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

예외가 발생하게 하도록 중복 행을 추가하지 않고 저장소에서 생성된 키를 사용하지 않도록 해야 합니다. 클라이언트에서 생성된 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);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Blogs.AsNoTracking().AnyAsync(b => b.BlogId == blogToAdd.BlogId, cancellationToken));

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);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Transactions.AsNoTracking().AnyAsync(t => t.Id == transaction.Id, cancellationToken));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
await db.SaveChangesAsync();

메모

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

추가 리소스