본문으로 건너뛰기
  1. Posts/

[MySQL] 동시성 제어 정리 — MVCC, 2PL, Locking Read, Next-Key Lock

NineKoo9
작성자
NineKoo9
목차

이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. InnoDB에서 일반 SELECT와 locking read의 차이, MVCC와 2PL의 관계, next-key lock과 undo/redo의 역할을 정리합니다.

목차
#

  1. MVCC
  2. 2PL
  3. MVCC와 2PL의 관계
  4. Isolation Level 심층 비교
  5. Non-Repeatable Read vs Phantom Read
  6. Locking Read와 Next-Key Lock
  7. Steal/No-Force 정책과 Redo/Undo Log의 관계
  8. 핵심 요약

1. MVCC
#

개념
#

MVCC는 데이터의 여러 버전을 활용해 읽기와 쓰기가 서로를 불필요하게 막지 않도록 하는 방식이다.

InnoDB의 일반 SELECTREAD COMMITTEDREPEATABLE READ 에서 기본적으로 consistent nonlocking read 로 처리된다. 즉, 보통은 현재 최신 값을 바로 읽기보다, 특정 시점의 스냅샷을 기준으로 보이는 버전을 읽는다.

InnoDB의 구현 - 숨겨진 컬럼과 undo log
#

MySQL 공식 문서 기준으로 InnoDB는 각 행에 내부 필드를 유지한다.

숨김 컬럼역할
DB_TRX_ID이 행을 마지막으로 INSERT 또는 UPDATE 한 트랜잭션 ID
DB_ROLL_PTRrollback segment의 undo log record를 가리키는 포인터
DB_ROW_ID내부 row id
현재 row
  DB_TRX_ID = 110
  DB_ROLL_PTR ──▶ undo log record
                     이전 버전 정보

핵심은 “행을 통째로 여러 개 복사해 둔다"기보다:

  • 현재 row에는 최신 버전이 있고
  • 필요하면 DB_ROLL_PTR 를 따라 undo 정보를 이용해 더 오래된 버전을 재구성한다

고 이해하는 편이 정확하다.

공식 문서는 삭제도 내부적으로는 delete mark가 붙는 update처럼 다룬다고 설명한다.

Read View
#

Read View는 “무엇이 보이고 무엇이 안 보이는지"를 결정하는 스냅샷 메타데이터다.

엄밀히는 활성 트랜잭션 목록과 visibility boundary를 기준으로 판단하므로, 흔히 말하는 “내 TRX_ID보다 작은 건 다 보인다” 정도로 단순화하면 부족하다. 다만 개념적으로는 다음처럼 이해해도 된다.

  • 스냅샷 시점보다 이전에 커밋된 버전 은 보인다
  • 그 시점에 아직 활성 중이거나 이후에 커밋된 버전 은 보이지 않는다
  • 현재 row가 보이지 않으면 InnoDB는 필요할 경우 undo를 이용해 더 오래된 버전을 재구성한다
  • 예외적으로, 현재 트랜잭션이 earlier statements에서 직접 만든 변경은 현재 트랜잭션에서 보인다

Isolation Level별 스냅샷 생성 시점
#

Isolation Level일반 SELECT 기준 동작
READ UNCOMMITTEDnonlocking read이지만 일관된 스냅샷 읽기는 아님
READ COMMITTED각 consistent read마다 fresh snapshot 생성
REPEATABLE READ첫 consistent read가 만든 snapshot 재사용
SERIALIZABLEautocommit=0 이면 plain SELECT 가 사실상 FOR SHARE 로 처리

MVCC가 주는 효과
#

READ COMMITTEDREPEATABLE READ 의 일반 SELECT 에서는 다음 성질이 핵심이다.

  • 커밋되지 않았거나 나중에 커밋된 변경은 보이지 않는다
  • consistent read는 읽는 동안 잠금을 잡지 않으므로 읽기-쓰기 충돌을 크게 줄인다
  • REPEATABLE READ 에서는 같은 트랜잭션 안의 repeated plain SELECT 가 서로 일관된 결과를 본다

2. 2PL
#

개념
#

2PL(Two-Phase Locking)은 락 획득 구간과 락 해제 구간을 분리하는 고전적 동시성 제어 모델이다.

Growing Phase          Shrinking Phase
(락 획득만 가능)         (락 해제만 가능)
──────────────────────────────────────────
  Lock A
  Lock B
  Lock C
               ──▶ Unlock A
                   Unlock B
                   Unlock C
  • Growing Phase: 필요한 락을 획득하기만 하고 해제하지 않음
  • Shrinking Phase: 락을 해제하기 시작하면 새로운 락을 획득하지 않음

InnoDB에서는 어떻게 봐야 하나
#

MySQL 공식 문서는 InnoDB transaction model이 multi-versioning database의 장점과 traditional two-phase locking을 결합 하려 한다고 설명한다.

따라서 InnoDB 전체를 곧바로 “Strict 2PL 데이터베이스"라고 부르면 과하지만, locking read와 쓰기 구간의 락 동작 만 떼어 보면 strict 2PL에 가까운 성격이 있다.

예를 들어 FOR UPDATE, FOR SHARE, UPDATE, DELETE 는 락을 잡고 트랜잭션 종료 시점까지 유지한다.

BEGIN;

SELECT * FROM orders WHERE id = 1 FOR UPDATE;
UPDATE orders SET status = 'DONE' WHERE id = 1;

COMMIT;

공식 문서는 FOR SHAREFOR UPDATE 로 잡은 락이 COMMIT 또는 ROLLBACK 시 해제된다고 명시한다.

InnoDB의 주요 락 종류
#

락 종류설명
Shared Lock (S)읽기 보호용 락
Exclusive Lock (X)수정/삭제용 락
Intention Lock (IS / IX)테이블 레벨 의도 표시
Gap Lock인덱스 레코드 사이 gap에 대한 락
Next-Key Lockindex-record lock + gap lock
Insert Intention LockINSERT 직전 gap에 잡히는 의도 락

락의 실제 범위는 격리 수준과 검색 조건, 사용한 인덱스에 따라 달라진다.

Deadlock
#

락 기반 동시성 제어의 대표적인 부작용은 데드락이다.

T1: Lock A → Lock B 시도
T2: Lock B → Lock A 시도

InnoDB는 deadlock detection을 통해 이런 순환 대기를 감지하고, 한 트랜잭션을 롤백해 문제를 푼다.


3. MVCC와 2PL의 관계
#

InnoDB는 둘 중 하나만 쓰는 엔진이 아니라, 읽기 경로와 잠금 경로를 분리해 함께 사용 한다.

작업사용 방식
일반 SELECT (READ COMMITTED, REPEATABLE READ)MVCC 기반 consistent nonlocking read
SELECT ... FOR UPDATE / SELECT ... FOR SHARElocking read, 최신 상태 + 락
UPDATE / DELETE최신 상태 + 락
INSERTinsert intention lock + record lock

그래서 InnoDB를 가장 무리 없이 설명하는 방식은 다음이다.

  • 일반 읽기 는 MVCC
  • locking read와 DML 은 락 기반
  • 락 기반 구간은 strict 2PL에 가까운 성격

공식 문서가 REPEATABLE READ 안에서 nonlocking SELECT 와 locking statement를 섞는 것을 권장하지 않는 이유도 여기 있다. 한 트랜잭션 안에 snapshot world와 current world가 공존 하기 때문이다.


4. Isolation Level 심층 비교
#

Read Committed - SELECT를 실행한 그 순간 기준
#

T1: BEGIN
T1: (t=1) SELECT ...  -> snapshot A 생성

  T2: UPDATE ... ; COMMIT  (t=2)

T1: (t=3) SELECT ...  -> snapshot B 생성
                         -> T2 변경이 반영될 수 있음

포인트는 단순하다.

  • 각 consistent read마다 새 snapshot을 만든다
  • 같은 트랜잭션 안에서도 repeated read 결과가 달라질 수 있다
  • locking read / UPDATE / DELETE 는 보통 gap이 아니라 index record만 잠근다
  • gap locking은 주로 foreign key check와 duplicate key check에서만 사용된다

즉, READ COMMITTED 에서는 range 기반 locking read에서 phantom row 문제가 남을 수 있다.


Repeatable Read - 첫 consistent read 기준
#

T1: BEGIN
T1: (t=1) SELECT ...  -> snapshot A 생성

  T2: UPDATE ... ; COMMIT  (t=2)

T1: (t=3) SELECT ...  -> snapshot A 재사용
                         -> plain SELECT 결과는 일관됨

포인트는 다음과 같다.

  • 첫 consistent read가 만든 snapshot을 재사용한다
  • 같은 트랜잭션 안의 plain SELECT 는 서로 일관된 결과를 본다
  • range 조건의 locking read, UPDATE, DELETE 는 gap lock 또는 next-key lock을 사용할 수 있다

다만 여기서도 주의할 점이 있다.

  • plain SELECT 는 read view 기반 snapshot을 본다
  • locking read와 DML은 latest state 를 기준으로 락을 사용한다

그래서 같은 트랜잭션 안에서 nonlocking SELECT 와 locking statement를 섞으면 해석이 어려워진다.


Serializable - “MVCC 완전 포기"라고 단정하면 과하다
#

원문처럼 SERIALIZABLE 을 “MVCC를 완전히 버린다"라고 쓰면 과장이다.

MySQL 8.4 공식 문서는 SERIALIZABLEREPEATABLE READ 와 유사하지만, autocommit 이 꺼져 있을 때 plain SELECT 를 암묵적으로 SELECT ... FOR SHARE 로 바꾼다고 설명한다.

즉:

  • 수동 트랜잭션에서는 plain SELECT 도 locking read처럼 동작한다
  • 하지만 autocommit 이 켜져 있으면 plain SELECT 는 자기 자신이 하나의 read-only transaction이므로 consistent nonlocking read로 직렬화될 수 있다

따라서 더 정확한 이해는 이렇다.

  • SERIALIZABLE = “항상 MVCC 완전 포기”
  • 가 아니라
  • “RR보다 더 엄격하고, 특히 autocommit off의 plain SELECT 는 locking read처럼 다룬다”

Isolation Level 전체 비교
#

Levelplain SELECTlocking read / DML특징
READ UNCOMMITTEDnonlocking read, but not consistent대체로 RC와 유사한 lockingdirty read 가능
READ COMMITTED각 consistent read마다 fresh snapshotgap locking 대부분 비활성repeated read 불안정
REPEATABLE READ첫 consistent read snapshot 재사용range scan 시 gap/next-key lock 사용 가능InnoDB 기본값
SERIALIZABLEautocommit off면 plain SELECT 가 사실상 FOR SHARE가장 보수적 locking동시성 저하 가능

5. Non-Repeatable Read vs Phantom Read
#

공통 전제 - plain SELECT 와 locking read를 구분해야 한다
#

이 두 이상 현상은 교과서적으로는 비슷해 보여도, InnoDB에서는 일반 consistent read인지, locking read인지 에 따라 설명이 달라진다.

5-1. Non-Repeatable Read - update된 기존 행
#

T1: BEGIN
T1: SELECT * FROM users WHERE id = 1  -> 'Alice'

  T2: UPDATE users SET name='Bob' WHERE id=1;
  T2: COMMIT

T1: SELECT * FROM users WHERE id = 1  -> ?

현재 row가 이미 바뀌어 있어도, REPEATABLE READ 의 plain SELECT 는 snapshot 기준에 맞는 더 오래된 버전을 찾아야 할 수 있다.

[현재 Row]
  id=1, name='Bob'
  DB_TRX_ID = 110
  DB_ROLL_PTR ──────────▶ [Undo Log]
                              id=1, name='Alice'
  • READ COMMITTED 에서는 두 번째 SELECT 가 새 snapshot을 잡으므로 Bob 을 볼 수 있다
  • REPEATABLE READ 에서는 첫 snapshot을 재사용하므로 update undo를 이용해 더 오래된 버전을 재구성할 수 있다

즉, 기존 행의 update 는 update undo 체인이 중요한 사례다.


5-2. Phantom Read - 범위 안에 새 행이 생기는 경우
#

T1: BEGIN
T1: SELECT * FROM users WHERE age > 20

  T2: INSERT INTO users VALUES (3, 'Charlie', 28);
  T2: COMMIT

T1: SELECT * FROM users WHERE age > 20  -> ?

여기서 주의할 점은 다음 두 가지다.

  1. REPEATABLE READplain SELECT 는 같은 snapshot을 재사용하므로, repeated read 결과 집합이 안정적이다.
  2. MySQL 공식 문서가 phantom problem을 설명할 때 사용하는 대표 예시는 SELECT ... FOR UPDATE 같은 locking read 다.

즉, “phantom은 insert된 행에 undo log가 없어서 생긴다"처럼 설명하면 부정확하다.

더 정확하게 말하면:

  • 새로 insert된 행은 내 snapshot 이후에 생긴 row라면 visible하지 않다
  • insert undo는 rollback 용도이고 commit 후 버릴 수 있다
  • consistent read에서 과거 버전 재구성에 핵심적으로 쓰이는 것은 update undo
  • locking read의 phantom 방지는 next-key lock 이 담당한다

5-3. 두 차이의 핵심 비교
#

구분Non-Repeatable ReadPhantom Read
관찰 대상같은 row의 값이 바뀜같은 predicate의 결과 집합이 바뀜
대표 원인기존 행 UPDATE범위 안으로 새 행 INSERT 또는 기존 행 이동
REPEATABLE READ plain SELECTsnapshot + update undo로 안정적같은 snapshot 재사용으로 결과 집합 안정적
locking read에서의 방어최신 상태에 대해 row lock 사용range scan에 next-key/gap lock 사용

핵심은 이렇다.

  • plain SELECT 의 반복 가능성 은 snapshot이 담당한다
  • locking read의 phantom 방지 는 next-key lock이 담당한다

6. Locking Read와 Next-Key Lock
#

Locking Read는 최신 상태와 락을 함께 쓴다
#

SELECT ... FOR UPDATESELECT ... FOR SHARE 는 plain SELECT 와 다르게 locking read다.

  • 최신 상태를 기준으로 읽는다
  • 검색 과정에서 만난 index record들에 락을 건다
  • 트랜잭션 종료 시까지 락을 유지한다

공식 문서는 old version은 잠글 수 없고, old version은 undo log를 적용해 메모리 상에서 재구성한다고 설명한다.

또한 locking read는 명시적 트랜잭션 안에서 써야 한다. 문서도 START TRANSACTION 이나 autocommit=0 상태를 전제로 설명한다.

Next-Key Lock이 필요한 이유
#

MySQL 공식 문서의 phantom 예시는 다음과 같다.

SELECT * FROM child WHERE id > 100 FOR UPDATE;

인덱스에 90, 102 만 있다고 하자.

T1: SELECT * FROM child WHERE id > 100 FOR UPDATE;
T2: INSERT INTO child VALUES (101);  -- gap이 안 잠기면 phantom 가능

이때 단순 row lock만으로는 (90, 102) 사이에 101 이 새로 들어오는 것을 막지 못한다. 그래서 InnoDB는 next-key locking 을 사용한다.

Next-Key Lock의 의미
#

next-key lock은:

  • index-record lock
    • 그 레코드 앞의 gap lock

의 조합이다.

(90, 102]   <- 102 record + 그 앞 gap
(102, +∞)   <- 마지막 record 뒤 gap

공식 문서는 기본 REPEATABLE READ 에서 InnoDB가 search와 index scan에 next-key lock을 사용해 phantom row를 막는다고 설명한다.

반대로 READ COMMITTED 에서는 gap locking이 대부분 비활성화되므로, range 조건 locking read에서는 phantom row 문제가 다시 가능해진다.


7. Steal/No-Force 정책과 Redo/Undo Log의 관계
#

배경 - Buffer Pool, Dirty Page, Checkpoint
#

InnoDB는 table과 index 데이터를 메모리의 Buffer Pool 에 캐시한다. 수정된 페이지는 곧바로 data file에 쓰이지 않을 수 있고, 이런 페이지를 dirty page라고 부른다.

[Disk]  ──읽기──▶  [Buffer Pool]
                      └─ dirty page
                           └─ background flush / checkpoint

공식 문서는:

  • dirty page가 background에서 flush될 수 있고
  • page cleaner가 이를 담당하며
  • checkpoint는 fuzzy checkpointing 으로 동작해 전체 버퍼 풀을 한 번에 flush할 필요가 없다고 설명한다

즉, commit과 data file flush 시점은 항상 1:1로 맞물려 있지 않다.

Redo Log
#

MySQL 8.4 공식 문서에서 redo log는:

  • disk-based data structure
  • crash recovery 때 data file에 끝까지 반영되지 못한 변경을 보정하기 위한 구조

로 설명된다.

즉, 예기치 않은 종료 전에 data file 갱신이 끝나지 못한 변경은 재시작 시 redo를 replay해 복원한다.

Undo Log
#

undo log record는 clustered index record의 최신 변경을 되돌리기 위한 정보 를 담는다.

공식 문서는 또 다음을 명확히 구분한다.

  • insert undo: rollback에만 필요하고 commit 후 버릴 수 있다
  • update undo: rollback뿐 아니라 consistent read에도 쓰이며, 더 이상 어떤 snapshot도 필요로 하지 않을 때만 버릴 수 있다

따라서 undo log의 역할은 두 갈래다.

  • rollback을 위한 복구 정보
  • consistent read를 위한 과거 버전 재구성 정보

Steal/No-Force라는 표현은 해석이다
#

여기서 원문의 Steal + No-Force 설명은 교과서적 해석으로는 꽤 유용하지만, MySQL 공식 문서가 InnoDB를 그 용어로 직접 규정하는 것은 아니다.

다만 공식 문서가 보여 주는 사실을 묶어 보면:

  • dirty page는 background에서 나중에 flush될 수 있고
  • redo는 crash recovery에 필요하며
  • undo는 rollback과 consistent read에 필요하다

는 점에서, 이론적으로는 steal/no-force에 가까운 운영 모델로 이해할 수 있다 고 말하는 정도는 가능하다.

즉 이 부분은 다음처럼 표현하는 편이 안전하다.

  • “InnoDB는 공식 문서상 Steal + No-Force라고 규정된다”
  • 보다는
  • “공식 문서가 설명하는 flush/recovery 동작을 교과서 용어로 읽으면 steal/no-force에 가깝게 이해할 수 있다”

8. 핵심 요약
#

plain SELECT in RC/RR
  -> MVCC 기반 consistent nonlocking read

READ COMMITTED
  -> consistent read마다 fresh snapshot

REPEATABLE READ
  -> 첫 consistent read snapshot 재사용

FOR UPDATE / FOR SHARE / UPDATE / DELETE
  -> 최신 상태 + 락
  -> locking part는 strict 2PL에 가까움

phantom 방지
  -> plain SELECT의 반복 가능성은 snapshot이 담당
  -> locking read의 phantom 방지는 next-key lock이 담당

SERIALIZABLE
  -> RR보다 더 엄격
  -> autocommit off의 plain SELECT는 사실상 FOR SHARE처럼 동작

redo / undo
  -> redo는 crash recovery replay
  -> undo는 rollback + old-version reconstruction

한 줄로 줄이면 이렇다.

InnoDB는 읽기를 전부 락으로 처리하는 엔진도 아니고, 전부 MVCC만 쓰는 엔진도 아니다. plain SELECT 는 snapshot을 읽고, locking read와 쓰기 작업은 최신 상태를 기준으로 락을 사용한다.

참고 자료
#