본문 바로가기

Database

[DB]MVCC(Multi-Version Concurrency Control), 동시성 제어란?

동시성 제어(Concurrency Control)

동시성 제어란?

동시성 제어란 DBMS가 다수의 사용자 사이에서 동시에 작용하는 다중 트랜잭션의 상호간섭 작용에서 데이터베이스를 보호하는 것을 의미합니다. 일반적으로 동시성을 허용하면 일관성이 낮아지게 됩니다.

 

일관성과 동시성의 관계

그래서 DBMS는 동시성을 제어할 수 있도록 LOCK 기능과 트랜잭션의 격리성 수준을 조정할 수 있는 SET TRANSACTION 명령어를 제공하고 있습니다.

이러한 동시성을 제어하는 방법에는 낙관적 동시성 제어비관적 동시성 제어가 있습니다.

낙관적 동시성 제어

  • 사용자들이 같은 데이터를 동시에 수정하지 않을 것이라고 가정.
  • 데이터를 읽은 시점에 LOCK을 걸지 않는 대신 수정 시점에 값이 변경되었는지 반드시 검사해야 합니다.

비관적 동시성 제어

  • 사용자들이 같은 데이터를 동시에 수정할 것이라고 가정
  • 데이터를 읽은 시점에 LOCK을 걸고, 트랜잭션이 완료될 때까지 이를 유지합니다.
  • SELECT 시점에 LOCK을 거는 비관적 동시성 제어는 동시성을 심각하게 떨어뜨릴 수 있어 WAIT 또는 NOWAIT 옵션과 함께 사용해야 합니다.

동시성 제어의 목표는 실행되는 트랜잭션의 수를 최대화하고 데이터의 무결성을 유지하는 데 있습니다. 따라서 동시 업데이트가 거의 없는 경우 낙관적 동시성 제어를사용하면 되지만, 그렇지 않다면 비관적 동시성 제어를 사용해야 합니다.

 

 

공유락(Shared Lock)과 배타락(Exclusive Lock)

비관적 동시성 제어를 위한 대표적인 LOCK에는 공유락(Shared Lock)배타락(Exclusive Lock)이 있습니다.

  • 공유락(Shared Lock): 읽기 잠금
  • 배타락(Exclusive Lock): 쓰기 잠금

 

동일한 레코드에 대해 각각 공유락과 배타락을 가져간 경우 동작은 다음과 같습니다.

  • A 트랜잭션이 공유락(Shared Lock)을 가져간 경우
    • B 트랜잭션이 읽기 작업을 할 경우 데이터가 일관되므로, B 트랜잭션은 또 다른 공유락을 가져가면서 동시에 처리하게 됩니다.
    • B 트랜잭션이 쓰기 작업을 할 경우 A 트랜잭션과 데이터가 달라질 수 있으므로 A 트랜잭션이 종료될 때 까지 대기합니다. 
  • A 트랜잭션이 배타락(Exclusive Lock)을 가져간 경우
    • B 트랜잭션이 읽기 작업을 하던지, 쓰기 작업을 하던지 A 트랜잭션이 데이터를 변경할 수 있으므로 대기합니다.

 

여기서 LOCK을 해제할 수 있는 방법은 COMMIT과 ROLLBACK 뿐입니다. 이러한 일반적인 LOCKING 메커니즘은 구현이 간단한 반면, 아래와 같은 문제점을 갖고 있습니다.

  • 읽기 작업과 쓰기 작업이 서로 방해를 일으키기 때문에 동시성 문제가 발생
  • 데이터 일관성에 문제가 생기는 경우도 있어서 LOCK을 더 오래 유지하거나 테이블 레베르이 LOCK을 사용해야 하고 동시성이 저하됩니다.

 

따라서, 이러한 문제점을 해결하기 위해 MVCC(Multi-Version Concurrency Control)가 탄생하게 되었습니다.


MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)

MVCC는 동시 접근이 가능한 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법입니다.

 

MVCC는 원본 데이터와 변경 중인 데이터를 동시에 유지하는 방식으로 원본 데이터의 Snapshot을 백업하여 보관합니다. 만약 두 가지 버전의 데이터가 존재하는 상황에서 새로운 사용자가 데이터에 접근하게 되면 원본 Snpashot을 읽게 됩니다. 그러다 변경이 취소되면 원본 Snapshot을 바탕으로 데이터를 복구하고, 만약 변경되었다면 최종적으로 디스크에 반영하는 방식으로 동작합니다.

 

결국 MVCC는 Snapshot을 이용하는 방식으로 기존의 데이터를 덮어 씌우는 게 아닌 기존 데이터를 바탕으로 이전 버전의 데이터와 비교하여 변경된 내용을 기록합니다. 이렇게 해서 하나의 데이터에 대해 여러 버전의 데이터가 존재하게 되고 사용자는 마지막 버전의 데이터를 읽게 됩니다. 따라서, MVCC의 특징은 다음과 같습니다.

 

  • 일반적인 RDBMS보다 매우 빠르게 작동합니다.
  • 사용하지 않는 데이터가 계속 쌓이게 되므로 데이터를 정리하는 시스템이 필요합니다.
  • 데이터 버전이 출동하면 애플리케이션 영역에서 이러한 문제를 해결해야 합니다.

 

MVCC의 접근 방식은 잠금을 필요로 하지 않기 때문에 일반적인 RDBMS보다 매우 빠르게 작동합니다. 또한, 데이터를 읽기 시작할 때, 다른 사람이 그 데이터를 삭제하거나 수정하더라도 영향을 받지 않고 데이터를 사용할 수 있습니다. 대신 사용하지 않는 데이터들이 쌓이게 되므로 데이터를 정리할 필요가 있습니다. MVCC 모델은 하나의 데이터에 대한 여러 버전의 데이터를 허용하기 때문에 데이터 버전이 충돌될 수 있으므로 애플리케이션 영역에서 이러한 문제를 해결해야 합니다. 또한, UNDO 블록 I/O, CR Copy 생성, CR 블록 캐싱 같은 부가적인 작업의 오버헤드가 발생합니다. 이러한 구조의 MVCC는 문장 수준과 트랜잭션 수준의 읽기 일관성이 존재합니다.

 

DBMS 별 MVCC 구현 방식

  1. PostgreSQL:
    • PostgreSQL은 각 트랜잭션이 시작될 때의 스냅샷을 유지하여, 읽기 작업이 다른 트랜잭션의 쓰기 작업에 영향을 받지 않도록 합니다. 각 행은 버전 정보를 가지고 있어, 트랜잭션이 완료되면 이전 버전이 삭제됩니다.
  2. MySQL (InnoDB):
    • InnoDB 스토리지 엔진은 각 행에 대한 두 개의 포인터(현재 버전, 이전 버전)를 유지하여 MVCC를 구현합니다. 트랜잭션이 시작될 때 스냅샷을 생성하고, 이를 통해 읽기 일관성을 제공합니다.
  3. Oracle:
    • Oracle은 MVCC를 구현하기 위해 Undo Segments를 사용합니다. 각 트랜잭션은 자신만의 Undo 정보를 생성하여, 다른 트랜잭션이 데이터를 읽을 때의 스냅샷을 제공합니다.
    • Oracle은 읽기 일관성을 보장하기 위해, 데이터가 변경될 때 기존 데이터의 복사본을 유지하여 트랜잭션이 완료될 때까지 다른 사용자가 해당 데이터에 접근할 수 있도록 합니다.
  4. Microsoft SQL Server:
    • SQL Server는 Snapshot Isolation을 통해 MVCC를 제공합니다. 트랜잭션이 시작될 때의 데이터 스냅샷을 생성하고, 이를 기반으로 읽기 작업을 수행합니다.

 

MySQL의 MVCC(Multi-Version Concurrency Control)

MySQL의 InnoDB에서는 Undo Log를 활용하여 MVCC를 구현합니다. 예를 들어 살펴보겠습니다.

아래와 같은 쿼리문이 실행되었다고 가정하겠습니다.

CREATE TABLE member (
    id INT NOT NULL,
    name VARCHAR(20) NOT NULL,
    area VARCHAR(100) NOT NULL,
    PRIMARY KEY(m_id),
    INDEX idx_area(area)
)

INSERT INTO member(id, name, area) VALUES (1, "Retto", "서울");

 

그럼 데이터는 다음과 같이 저장됩니다. 메모리와 디스크에 모두 데이터가 동일하게 저장됩니다.

 

그리고 다음과 같은 UPDATE 문을 실행시켰다고 해봅시다.

UPDATE member SET area = "경기" WHERE id = 1;

 

UPDATE문이 실행된 결과는 다음과 같습니다. 먼저 COMMIT 실행 여부와 무관하게 InnoDB 버퍼 풀은 새로운 값으로 갱신 됩니다. 그리고 UNDO LOG에는 변경 전의 값들만 복사됩니다. 또한, InnoDB 버퍼 풀의 내용은 백그라운드 쓰레드를 통해 디스크에 기록되는데, 디스크에도 반영되었는지 여부는 시점에 따라 다를 수 있어서 ?로 표시해두었습니다.

 

COMMIT이나 ROLLBACK이 호출되지 않은 상태에서 다른 사용자가 아래와 같은 쿼리로 데이터를 조회하면 어떤 결과가 반환될까요?

SELECT * FROM member WHERE id = 1;

그 결과는 트랜잭션의 격리 수준(Isolation Level)에 따라 달라집니다.

만약, COMMIT 되지 않는 내용도 조회하도록 해주는 READ_UNCOMMITED라면 버퍼 풀의 데이터를 읽어서 반환하며, 이는 COMMIT 여부와 무관하게 변경된 데이터를 읽어 반환하는 것입니다.

READ_COMMITED나 그 이상의 격리 수준(REPEATABLE_READ, SERIALIZABLE)이라면 변경되기 이전의 UNDO LOG 영역의 데이터를 반환하게 됩니다. 이것이 가능한 이유는 하나의 데이터에 대해 여러 버전을 관리하는 MVCC 덕분입니다.

여기서 UNDO LOG 영역의 데이터는 COMMIT 혹은 ROLLBACK을 호출하여 InnoDB 버퍼 풀도 이전의 데이터로 복구되고 더 이상 UNDO 영역을 필요로 하는 트랜잭션이 더는 없을 때 비로소 삭제되어 집니다.


참고

https://mangkyu.tistory.com/53