해당 글은 MySQL에서 InnoDB 엔진을 사용했을 때의 동작 원리를 기준으로 작성되었습니다.
InnoDB가 아닌 다른 스토리지 엔진(ex. MyISAM)에선 존재하지 않는 개념일 수 있습니다.
안녕하세요 머랭입니다.
오늘은 데이터베이스의 핵심 기능 중 하나인 MVCC(다중 버전 동시성 제어), 그리고 MVCC 구현의 중심에 있는 Read View에 대해 알아보겠습니다.
백엔드 개발자라면 "DB 트랜잭션"이라는 말을 지겹게 들었을 겁니다.
여러 사용자가 동시에 데이터를 읽고 쓸 때, 데이터의 일관성을 지키는 것은 정말 중요한데요.
일관성을 지키기 위해 사용할 수 있는 가장 단순한 방법은 락(Lock)입니다.
일관성을 지키기 위한 간단한 방법 - 베타 락과 공유 락
트랜잭션 A가 R1(레코드)에 대해 읽기/쓰기 작업 중입니다. A는 자신이 커밋되기 전까지 R1의 데이터 일관성을 보장받고 싶어 락을 걸 수 있습니다.
A가 R1에 락을 걸게 되면, 다른 트랜잭션들은 R1에 대한 작업을 하기 위해 대기해야 합니다.
레코드(R1)에 걸 수 있는 락은 두 종류가 있습니다.
베타 락 (Exclusive Lock, X - Lock)
베타 락은 레코드에 대한 쓰기, 베타 락 생성, 공유 락 생성을 막습니다.
공유 락(Shared Lock, S - Lock)
공유 락은 레코드에 대한 쓰기, 베타 락 생성을 막습니다.
베타 락이 다른 락 생성도 막는 것에 비해, 공유 락은 다른 공유 락 생성은 허용합니다.
락의 단점
락을 사용하면 가장 강한 일관성을 얻을 수 있지만, 극복할 수 없는 단점이 있습니다. 락을 생성하기 위해 기다리는 시간이 길어질수록 서비스 전체의 성능(동시 처리)이 떨어지게 됩니다.
특히 단순히 데이터를 읽기만 하려는 작업까지 쓰기 작업을 기다려야 한다면 너무 비효율적입니다.
-> 공유 락을 획득하기 위해 베타 락이 해제될 때까지 기다려야 한다면 너무 비효율적입니다.
이 문제를 해결하기 위해 등장한 기술이 바로 MVCC(다중 버전 동시성 제어)입니다.
MVCC(다중 버전 동시성 제어)
MVCC는 데이터의 버전을 여러 개로 관리하는 방식을 도입해 락의 성능 문제를 해결합니다.
MVCC는 단순 읽기 작업에서 락 없는 읽기를 제공합니다.
1. MVCC는 베타 락으로 인한 성능 문제를 해결합니다
A 트랜잭션에서 R1(레코드)에 대한 베타 락을 생성했습니다.
B 트랜잭션은 A 트랜잭션의 진행 상황과 상관없이 단순히 R1에 최신 커밋된 값만 읽고 싶습니다.
MVCC가 없다면, B 트랜잭션은 "단순 읽기"만 하고 싶음에도 불구하고 R1의 공유 락을 생성해야 하므로, A 트랜잭션이 커밋될 때까지 대기해야 합니다.
MVCC가 있다면, B 트랜잭션은 공유 락 없는 읽기가 가능하기 때문에, A 트랜잭션이 커밋되는 것을 기다리지 않아도 됩니다.
2. MVCC는 공유 락으로 인한 성능 문제를 해결합니다
A 트랜잭션은 R1(레코드)의 값을 단순히 읽고 싶지만, 공유 락을 생성해야 합니다.
B 트랜잭션은 R1의 값을 변경하기 위해 베타 락을 생성하고 싶습니다.
MVCC가 없다면, A 트랜잭션이 "단순 읽기"만 하고 싶음에도 불구하고 공유 락을 생성해야 합니다.
이로 인해 A 트랜잭션이 커밋되기 전까지 어떠한 트랜잭션도 베타 락을 생성할 수 없기 때문에, B 트랜잭션은 A 트랜잭션이 커밋될 때까지 대기해야 합니다.
MVCC가 있다면, A 트랜잭션은 공유 락 없는 읽기가 가능하기 때문에, B 트랜잭션은 A 트랜잭션이 커밋되는 것을 기다리지 않아도 됩니다.
이것이 가능한 이유는, MVCC가 "트랜잭션 내에서 읽기 작업을 하는 시점"에 트랜잭션이 읽어야 할 데이터 버전이 무엇인지 정확하게 판단하는 메커니즘을 가지고 있기 때문입니다.
MVCC는 Read View를 통해 트랜잭션이 읽어야 할 데이터 버전을 판단합니다.
Read View
Read View는 특정 트랜잭션이 특정 시점에 볼 수 있는 데이터의 버전을 판단하기 위한 자료구조입니다.
Read View에는 대표적으로 4가지 정보가 저장됩니다.
1. m_ids
현재 시점에서 활성 중인(아직 커밋되지 않은) 트랜잭션 ID 목록입니다.
2. m_up_limit_id
m_ids 목록에 있는 트랜잭션 ID 중 가장 작은 ID입니다.
m_up_limit_id보다 작은 ID는 모두 과거에 커밋되었음을 의미합니다.
3. m_low_limit_id
Read View 생성 시점까지 시스템이 할당한 마지막 트랜잭션 ID + 1.
m_low_limit_id보다 크거나 같은 ID는 미래에 시작될 트랜잭션임을 의미합니다.
4. m_creator_trx_id
Read View를 생성한 트랜잭션의 ID입니다.
MVCC가 Read View를 사용하는 방식
MVCC가 Read View를 사용해 락 없는 읽기를 가능하게 해 준다는 것은 알았습니다.
이제 어떤 과정을 통해 락 없는 읽기가 가능한지 알아보겠습니다.
MySQL은 레코드가 변경될 때마다 이전 레코드의 데이터를 Undo Log라는 곳에 저장해 버저닝합니다. 이 Undo Log들은 링크드 리스트처럼 구성됩니다.

MVCC는 Undo Log와 Read View를 사용해 락 없는 읽기를 지원합니다.
B 트랜잭션이 Read View를 가지고 데이터를 읽으려 할 때, 다음 과정이 수행됩니다.
Read View를 사용한 데이터 읽기 과정

1. 가장 최근의 Undo Log를 확인합니다.
2. Undo Log의 트랜잭션 ID를 확인하고, Read View를 기준으로 "이 버전을 봐도 되는지" 판단합니다.
버전을 봐도 되는지 판단하는 기준은 아래에서 설명하겠습니다.
3. 볼 수 없는 버전이라면 Undo Log 체인을 타고 이전 버전으로 이동합니다.
4. 볼 수 있는 버전을 만날 때까지 2~3번 과정을 반복합니다.
Undo Log 판단 메커니즘
Read View가 Undo Log를 만났을 때, "이 버전을 반환할지, 아니면 더 과거로 갈지" 판단하는 기준은 다섯 가지가 있습니다.
1. 내 작업 기록은 본다.
Undo Log의 트랜잭션 id가 m_creator_trx_id와 같다면 반환합니다.
Read View를 생성한 트랜잭션이 작성한 Undo Log이기 때문입니다.
2. 오래된 과거 작업 기록은 본다.
Undo Log의 트랜잭션 id가 m_up_limit_id보다 작다면 반환합니다.
Read View를 생성한 시점 이전의 트랜잭션이 작성한 Undo Log이기 때문입니다.
3. 미래 작업 기록은 보지 않는다.
Undo Log의 트랜잭션 id가 m_low_limit_id보다 크거나 같다면 반환하지 않습니다.
Read Veiw를 실행한 시점 이후에 열린 트랜잭션이 생성한 Undo Log이기 때문입니다.
4. 현재 진행 중인 트랜잭션의 작업 기록은 보지 않는다.
Undo Log의 트랜잭션 id가 m_ids 내에 존재한다면 반환하지 않습니다.
m_ids에 존재한다는 것은, Read View를 생성할 당시에 커밋되지 않은 트랜잭션임을 의미하기 때문입니다.
5. 가까운 과거의 작업은 본다.
Undo Log의 트랜잭션 id가 m_up_limit_id 보다는 크지만, m_ids 에는 존재하지 않는다면 반환합니다.
트랜잭션은 순서대로 닫히지 않기 때문으로,
m_ids = [4, 6, 7]
m_up_limit_id = [4]
Undo Log's trx id = 5인 경우가 해당됩니다.
부록 - MVCC를 사용하는 트랜잭션 격리 레벨
MVCC와 Read View는 공유 락 없는 읽기를 가능하게 함과 동시에, "Read View를 생성한 시점"에 커밋된 데이터만 보게 해 준다는 특징이 있습니다.
이러한 특징 덕에 MVCC는 의외의 곳에서 사용됩니다.
바로 트랜잭션 격리 수준인데요, READ_COMMITTED와 REPETABLE_READ입니다.
READ_COMMITTED
READ_COMMITTED는 "커밋된 데이터만 읽기"를 제공하기 위해, 매 SELECT 시마다 Read View를 생성합니다.
Read View를 사용한 MVCC의 읽기 규칙이 적용되어 커밋된 데이터만 읽을 수 있게 됩니다.
핵심 규칙!
4. 현재 진행 중인 트랜잭션의 작업 기록은 보지 않는다.
REPETABLE_READ
REPETABLE_READ는 "한 트랜잭션 내에서는 항상 동일한(일관된) 데이터만 읽기"를 제공하기 위해, 첫 SELECT에 Read View를 생성하고 트랜잭션 커밋 전까지 재사용합니다.
Read View를 사용한 MVCC의 읽기 규칙이 적용되어 커밋된 데이터만 읽을 수 있게 됩니다.
핵심 규칙!
3. 미래 작업 기록은 보지 않는다.
마무리
MVCC와 Read View 덕분에 데이터베이스는 단순 읽기 작업을 위해 공유 락을 거는 대신, 조회 시점에 적절한 데이터 버전을 보여줄 수 있습니다.
이로 인해 읽기 작업은 쓰기 작업을, 쓰기 작업은 읽기 작업을 기다리지 않게 되어 데이터베이스의 동시 처리 능력이 크게 향상됩니다.
'DB' 카테고리의 다른 글
| 캐싱, 개념과 읽기 전략들 (0) | 2024.06.16 |
|---|