Database

[DB] MVCC (Multiversion Concurrency Control)

kyoulho 2024. 8. 17. 17:49

MVCC의 핵심은 데이터의 여러 버전을 유지하여 트랜잭션이 시작된 시점에 맞춰 데이터를 읽을 수 있도록 하는 것이다. 이를 통해 각 트랜잭션은 자신이 시작된 시점의 데이터 스냅샷을 사용하여 일관성 있는 읽기를 보장받게 되며, 읽기 작업과 쓰기 작업이 서로를 차단하지 않는 환경을 가능하게 한다. 즉, 한 트랜잭션이 데이터를 수정하는 동안 다른 트랜잭션이 그 데이터를 읽더라도 서로 간섭하지 않게 된다.

이러한 MVCC의 동작은 데이터베이스 성능을 크게 향상시키는 장점이 있지만, 각 데이터에 대한 추가적인 버전 관리를 필요로 하므로 이를 효율적으로 처리하기 위한 메커니즘이 중요하다.

 

격리 수준과 MVCC의 적용

트랜잭션의 격리 수준(Isolation Level)은 여러 트랜잭션이 동시에 실행될 때 데이터의 일관성을 어떻게 유지할지를 결정하는 중요한 요소이다. MVCC는 이러한 격리 수준에 따라 다르게 적용될 수 있다.

1. Read Uncommitted

  • 특징: 가장 낮은 격리 수준으로, 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있다.
  • MVCC 적용: 일반적으로 MVCC가 적용되지 않는다. 이 격리 수준에서는 다른 트랜잭션의 커밋 여부와 상관없이 데이터를 읽을 수 있으며, 이는 "더티 리드"를 허용하게 된다.

2. Read Committed

  • 특징: 트랜잭션이 커밋된 데이터만 읽을 수 있다.
  • MVCC 적용: 트랜잭션이 데이터를 읽을 때 그 시점에 커밋된 데이터만 읽도록 보장한다. 이는 "더티 리드"를 방지하지만, "non-repeatable read" (한 트랜잭션 내에서 동일한 쿼리가 다른 결과를 반환하는 상황)가 발생할 수 있다.

3. Repeatable Read

  • 특징: 트랜잭션이 시작된 시점 기준으로 커밋된 데이터만 읽는다. 동일한 트랜잭션 내에서는 항상 같은 데이터를 읽는다.
  • MVCC 적용: 트랜잭션이 시작된 시점의 데이터 스냅샷을 기준으로 읽기 작업을 수행하며, 트랜잭션이 진행되는 동안 데이터의 일관성을 유지한다. 이를 통해 "non-repeatable read"를 방지하지만, 여전히 "팬텀 리드" (트랜잭션 중간에 새로운 데이터가 삽입되는 상황)가 발생할 수 있다.
  • 예시:
    • 초기값: x = 10
    • 트랜잭션 T2가 x에 대한 write-lock을 획득하고 값을 50으로 변경한 후 커밋한다.
    • 트랜잭션 T1이 x를 처음 조회했을 때의 값은 10이다. T2가 커밋된 후에도 T1은 계속 10을 읽는다.

4. Serializable

  • 특징: 가장 높은 격리 수준으로, 트랜잭션이 직렬적으로 실행된 것과 같은 결과를 보장한다.
  • MVCC 적용: 이 격리 수준에서는 MVCC가 아닌 잠금 메커니즘을 사용하여 직렬성을 보장하는 경우가 많다. MVCC를 사용할 수 있지만, 직렬성을 보장하기 위해 추가적인 잠금이나 순차적 실행이 필요할 수 있다.
  • 예시:
    • 초기값: x = 10
    • 트랜잭션 T2가 x에 대한 write-lock을 획득하고 값을 50으로 변경한 후 커밋한다.
    • 트랜잭션 T1이 x를 조회할 때, MVCC가 아닌 잠금을 사용하여 직렬성을 보장하므로, T1은 T2가 커밋한 후의 값을 읽을 수도, 이전 상태의 값을 읽을 수도 있다. 이는 시스템에 따라 다를 수 있다.

 

 

MVCC 적용된 Read Committed에서의 Lost Update 문제

Lost Update는 두 개 이상의 트랜잭션이 동일한 데이터를 읽고 수정한 후 각각의 결과를 커밋하는 과정에서 발생하는 문제로, 나중에 커밋된 트랜잭션이 이전의 수정 내용을 덮어쓰게 되는 상황을 말한다. MVCC가 적용된 Read Committed 격리 수준에서도 이러한 문제가 발생할 수 있다.

예시:

  • 초기값: x = 10
  • 트랜잭션 T1이 x = 10을 읽고, x = 20으로 수정한다.
  • 동시에, 트랜잭션 T2도 x = 10을 읽고, x = 30으로 수정한다.
  • T1이 x = 20을 커밋한다.
  • T2가 x = 30으로 수정한 후 커밋한다.

이 과정에서 T2의 커밋이 T1의 커밋을 덮어써서, 최종 값은 x = 30이 되며, T1의 업데이트가 사라지게 된다.

PostgreSQL에서의 해결 방법

PostgreSQL은 Repeatable Read 격리 수준에서 문제를 해결할 수 있다. 이 격리 수준에서는 MVCC를 통해 트랜잭션이 시작된 시점의 스냅샷을 기반으로 데이터를 읽는다. 그러나 같은 데이터를 업데이트하려는 트랜잭션이 있을 경우, PostgreSQL은 "first-updater-wins" 정책을 적용하여 먼저 커밋한 트랜잭션이 업데이트를 완료하고, 나중에 시도하는 트랜잭션은 롤백된다.

해결 방법:

  • T1과 T2가 동시에 동일한 데이터를 업데이트하려고 시도할 때, 먼저 커밋한 트랜잭션(T1)이 업데이트를 성공적으로 완료한다.
  • 이후 커밋을 시도하는 트랜잭션(T2)은 Serialization Failure를 발생시키며 롤백된다. T2는 다시 트랜잭션을 시도해야 한다.

MySQL에서의 해결 방법

MySQL의 Repeatable Read 격리 수준에서는 Lost Update 문제를 완벽하게 방지하지 못한다. MySQL에서 Serializable 격리 수준을 사용하거나, Locking Read (애플리케이션 레벨에서 명시적인 잠금)를 활용하는 방법이 필요하다.

해결 방법:

  1. Serializable 격리 수준 사용: 트랜잭션을 직렬적으로 처리하여 Lost Update를 방지한다.
  2. Locking Read:
    • SELECT … FOR UPDATE: 트랜잭션이 데이터를 업데이트하기 전에 명시적으로 SELECT ... FOR UPDATE를 사용하여 write-lock을 획득함으로써 다른 트랜잭션이 동시에 데이터를 수정하지 못하도록 한다.
    • SELECT … FOR SHARE: 데이터의 공유 잠금을 설정하여 다른 트랜잭션이 해당 데이터에 대해 배타적인 잠금을 설정하지 못하도록 방지한다.
    • Locking Read는 격리 수준과 관계없이 가장 최근에 커밋된 데이터를 읽는다.

 

MVCC 적용된 Repeatable Read에서의 Write Skew 문제

Write SkewRepeatable Read 격리 수준에서 발생할 수 있는 문제로, 두 개 이상의 트랜잭션이 서로의 상태를 읽고 이를 기반으로 업데이트를 수행하면서 발생한다. 각 트랜잭션이 자신이 시작할 때의 스냅샷을 기반으로 작업하지만, 서로의 상태를 반영하지 못하기 때문에 발생한다.

Write Skew 문제의 예시

병원에서 최소 두 명의 의사가 근무해야 한다고 가정해 보자. 만약 의사들이 교대 신청을 하면서 num_doctors라는 변수를 사용하여 현재 근무 중인 의사 수를 추적하고 있다면, Repeatable Read 격리 수준에서는 다음과 같은 문제가 발생할 수 있다.

시나리오:

  • 초기 값: num_doctors = 2 (병원에 근무 중인 의사 수)
  • 트랜잭션 T1: Dr. A가 교대 신청을 하기 위해 시작
    1. num_doctors를 읽어 현재 값이 2임을 확인 (num_doctors = 2).
    2. 교대가 가능하다고 판단하고, num_doctors를 1 감소시켜 1로 설정 (num_doctors = 1).
    3. 트랜잭션 T1을 커밋한다.
  • 트랜잭션 T2: Dr. B가 교대 신청을 하기 위해 시작
    1. num_doctors를 읽어 현재 값이 2임을 확인 (num_doctors = 2).
    2. 교대가 가능하다고 판단하고, num_doctors를 1 감소시켜 1로 설정 (num_doctors = 1).
    3. 트랜잭션 T2를 커밋한다.

결과:

  • 두 트랜잭션이 모두 커밋되면 num_doctors 값이 0으로 감소하게 된다.

PostgreSQL에서의 해결 방법

PostgreSQL에서는 Serializable 격리 수준을 사용하여 문제를 방지할 수 있다.

MySQL에서의 해결 방법

MySQL에서는 다음과 같은 방법을 사용할 수 있다:

  1. Serializable 격리 수준 사용
  2. Locking Read: 트랜잭션이 데이터를 읽을 때 다른 트랜잭션이 동시에 데이터를 수정하지 못하도록 하여 해결할 수 있다.
728x90

'Database' 카테고리의 다른 글

[DB] DBCP (Database Connection Pooling)  (0) 2024.08.17
[DB] Lock & 2PL  (0) 2024.08.17
[DB] Isolation Level  (0) 2024.08.16
[DB] Recoverability  (0) 2024.08.16
[DB] Serializability  (0) 2024.08.16