대용량 데이터 처리시 발생 하는 동시성 충돌 처리

2024. 3. 18. 00:00EntityFramewok Core (EF Core)

반응형

DB 와 관련된 Transaction 처리를 하다 보면 동시성 충돌 이라는 문제를 접하게 된다. 

오늘은 동시성 충돌이 무엇이고 EF Core 에서 어떻게 해결해야 하는지 알아보겠다. 

 

데이터베이스에서 동시성 충돌은 동시에 여러 트랜잭션이 같은 데이터나 자원을 변경하려고 할 때 발생하는 문제이다. 이는 데이터베이스의 일관성과 무결성을 해치는 문제로, 데이터베이스 시스템에서 주의해야 할 중요한 측면 중 하나이다.

동시성 충돌은 주로 다음과 같은 상황에서 발생할 수 있다:

  1. 동시성 제어 문제: 여러 트랜잭션이 동시에 데이터를 읽거나 쓰는 경우 발생할 수 있다. 이때 한 트랜잭션이 데이터를 변경하고 있는 동안 다른 트랜잭션이 동일한 데이터를 변경하려고 하면 충돌이 발생한다.
  2. 교착 상태(Deadlock): 두 개 이상의 트랜잭션이 서로가 소유한 자원을 대기하고 있어 무한 대기 상태에 빠지는 현상이다.
  3. 읽기-쓰기 충돌: 한 트랜잭션이 데이터를 읽으려고 할 때 다른 트랜잭션이 이미 그 데이터를 변경하고 있는 경우 발생한다.
  4. 쓰기-쓰기 충돌: 두 개 이상의 트랜잭션이 동일한 데이터를 변경하려고 할 때 발생한다.

이러한 충돌을 해결하기 위해서는 데이터베이스 관리 시스템(DBMS)이 적절한 동시성 제어 기법을 사용해야 한다. 이를 통해 트랜잭션 간의 충돌을 감지하고 관리하여 데이터 일관성을 유지할 수 있다. 주요한 동시성 제어 기법으로는 Locking, MVCC(Multi-Version Concurrency Control), Snapshot Isolation 등이 있다.

 

 

EF Core는 동시성 충돌이 비교적 드물다고 가정하는 낙관적 동시성을 구현한다. 데이터를 미리 잠그고 수정을 진행하는 비관적 접근 방식과 달리 낙관적 동시성은 잠금을 사용하지 않지만 쿼리된 이후 데이터가 변경된 경우 저장 시 데이터 수정이 실패하도록 준비한다. 이 동시성 오류는 새 데이터에 대한 전체 작업을 다시 시도하여 해당 오류를 처리하는 애플리케이션에 보고된다. EF Core에서 낙관적 동시성은 속성을 동시성 토큰으로 구성하여 구현된다. 동시성 토큰은 다른 속성과 마찬가지로 엔터티를 쿼리할 때 로드되고 추적된다. 그런 다음 SaveChanges() 중에 업데이트 또는 삭제 작업이 수행되면 데이터베이스의 동시성 토큰 값은 EF Core에서 읽은 원래 값과 비교된다.

 

 

다음과 같이 Modle 을 정의해 보자 

public class User
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Name { get; set; }
    public string Password { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    [Timestamp] // concurrency check 을 위해 속성설정
    public byte[] Version { get; set; }
}

위 코드에서 Version 은 SqlServer 또는 MySql 에서 동시성 충돌을 감지할 때 사용하게 된다. 

 

이제 동시성 충돌을 일으켜 보자

    var user = _dbContext.Users.Where(u => u.Email == command.Email).FirstOrDefault();
    if (user == null) throw new Exception("Could not find user");

    user.Age  = user.Age > 0 ? user.Age -1 : throw new Exception("Age is == 0");
    _dbContext.Database.ExecuteSqlRaw($"UPDATE `efcore-test`.Users SET Age = Age-1 WHERE Email = 'test2' and Age > 0 and Id='{user.Id}'");

    ...
    _dbContext.Users.Update(user);
    _dbContext.SaveChanges();

 

user 를 찾아서 age 를 update 하기 전에  ExecuteSqlRaw 를 이용해서 age 를 외부에서 Update 한 효과를 내자

이렇게 하면 test2 의 age 의 값이 가져온 값과 달라지게 된다. 

 

update 후 savechanges 하면 이제 DbUpdateConcurrencyException 이 발생하게 된다. 

 

 

이제 이 문제를 해결해 보자 

while(true)
{
     try
     {
         _dbContext.Users.Update(user);
         _dbContext.SaveChanges();
         break;
     }
     catch (DbUpdateConcurrencyException ex)
     {


         foreach (var entry in ex.Entries)
         {
             if (entry.Entity is Infrastructure.Models.User)
             {
                 var proposedValues = entry.CurrentValues;
                 var databaseValues = entry.GetDatabaseValues();

                 foreach (var property in proposedValues.Properties)
                 {
                     var proposedValue = proposedValues[property];
                     var databaseValue = databaseValues[property];

                     // TODO: decide which value should be written to database
                     if (property.Name.Equals("Age", StringComparison.OrdinalIgnoreCase))
                         proposedValues[property] = (proposedValues[property] as int?) > 0 ? (proposedValues[property] as int?) - 1 : throw new Exception("Age is == 0");

                 }

                 // Refresh original values to bypass next concurrency check
                 entry.OriginalValues.SetValues(databaseValues);
             }
             else
             {
                 throw new NotSupportedException(
                     "Don't know how to handle concurrency conflicts for "
                     + entry.Metadata.Name);
             }
         }
     }
}

 

위와 같이 설정하게 되면 동시성 충돌이 일어났을때 다시 최신 db 값을 가져와서 실행하게 된다. 

그런데 가져온 값이 (여기서는 age) 0 보다 작거나 같으면 error 가 발생하고 더이상 진행하지 않는다.

 

 

 

관련영상

https://youtu.be/Fbyaz8WXowo

반응형