이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. InnoDB에서 일반 SELECT와 locking read의 차이, MVCC와 2PL의 관계, next-key lock과 undo/redo의 역할을 정리합니다.
목차#
- MVCC
- 2PL
- MVCC와 2PL의 관계
- Isolation Level 심층 비교
- Non-Repeatable Read vs Phantom Read
- Locking Read와 Next-Key Lock
- Steal/No-Force 정책과 Redo/Undo Log의 관계
- 핵심 요약
1. MVCC#
개념#
MVCC는 데이터의 여러 버전을 활용해 읽기와 쓰기가 서로를 불필요하게 막지 않도록 하는 방식이다.
InnoDB의 일반 SELECT 는 READ COMMITTED 와 REPEATABLE READ 에서 기본적으로 consistent nonlocking read 로 처리된다. 즉, 보통은 현재 최신 값을 바로 읽기보다, 특정 시점의 스냅샷을 기준으로 보이는 버전을 읽는다.
InnoDB의 구현 - 숨겨진 컬럼과 undo log#
MySQL 공식 문서 기준으로 InnoDB는 각 행에 내부 필드를 유지한다.
| 숨김 컬럼 | 역할 |
|---|---|
DB_TRX_ID | 이 행을 마지막으로 INSERT 또는 UPDATE 한 트랜잭션 ID |
DB_ROLL_PTR | rollback 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 UNCOMMITTED | nonlocking read이지만 일관된 스냅샷 읽기는 아님 |
READ COMMITTED | 각 consistent read마다 fresh snapshot 생성 |
REPEATABLE READ | 첫 consistent read가 만든 snapshot 재사용 |
SERIALIZABLE | autocommit=0 이면 plain SELECT 가 사실상 FOR SHARE 로 처리 |
MVCC가 주는 효과#
READ COMMITTED 와 REPEATABLE READ 의 일반 SELECT 에서는 다음 성질이 핵심이다.
- 커밋되지 않았거나 나중에 커밋된 변경은 보이지 않는다
- consistent read는 읽는 동안 잠금을 잡지 않으므로 읽기-쓰기 충돌을 크게 줄인다
REPEATABLE READ에서는 같은 트랜잭션 안의 repeated plainSELECT가 서로 일관된 결과를 본다
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 SHARE 와 FOR UPDATE 로 잡은 락이 COMMIT 또는 ROLLBACK 시 해제된다고 명시한다.
InnoDB의 주요 락 종류#
| 락 종류 | 설명 |
|---|---|
Shared Lock (S) | 읽기 보호용 락 |
Exclusive Lock (X) | 수정/삭제용 락 |
Intention Lock (IS / IX) | 테이블 레벨 의도 표시 |
| Gap Lock | 인덱스 레코드 사이 gap에 대한 락 |
| Next-Key Lock | index-record lock + gap lock |
| Insert Intention Lock | INSERT 직전 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 SHARE | locking read, 최신 상태 + 락 |
UPDATE / DELETE | 최신 상태 + 락 |
INSERT | insert 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 공식 문서는 SERIALIZABLE 을 REPEATABLE READ 와 유사하지만, autocommit 이 꺼져 있을 때 plain SELECT 를 암묵적으로 SELECT ... FOR SHARE 로 바꾼다고 설명한다.
즉:
- 수동 트랜잭션에서는 plain
SELECT도 locking read처럼 동작한다 - 하지만
autocommit이 켜져 있으면 plainSELECT는 자기 자신이 하나의 read-only transaction이므로 consistent nonlocking read로 직렬화될 수 있다
따라서 더 정확한 이해는 이렇다.
SERIALIZABLE= “항상 MVCC 완전 포기”- 가 아니라
- “RR보다 더 엄격하고, 특히 autocommit off의 plain
SELECT는 locking read처럼 다룬다”
Isolation Level 전체 비교#
| Level | plain SELECT | locking read / DML | 특징 |
|---|---|---|---|
READ UNCOMMITTED | nonlocking read, but not consistent | 대체로 RC와 유사한 locking | dirty read 가능 |
READ COMMITTED | 각 consistent read마다 fresh snapshot | gap locking 대부분 비활성 | repeated read 불안정 |
REPEATABLE READ | 첫 consistent read snapshot 재사용 | range scan 시 gap/next-key lock 사용 가능 | InnoDB 기본값 |
SERIALIZABLE | autocommit 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 -> ?여기서 주의할 점은 다음 두 가지다.
REPEATABLE READ의 plainSELECT는 같은 snapshot을 재사용하므로, repeated read 결과 집합이 안정적이다.- 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 Read | Phantom Read |
|---|---|---|
| 관찰 대상 | 같은 row의 값이 바뀜 | 같은 predicate의 결과 집합이 바뀜 |
| 대표 원인 | 기존 행 UPDATE | 범위 안으로 새 행 INSERT 또는 기존 행 이동 |
REPEATABLE READ plain SELECT | snapshot + 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 UPDATE 와 SELECT ... 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와 쓰기 작업은 최신 상태를 기준으로 락을 사용한다.
참고 자료#
- MySQL 8.4 Reference Manual, InnoDB Transaction Model
https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-model.html - MySQL 8.4 Reference Manual, InnoDB Multi-Versioning
https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html - MySQL 8.4 Reference Manual, Consistent Nonlocking Reads
https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html - MySQL 8.4 Reference Manual, Transaction Isolation Levels
https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html - MySQL 8.4 Reference Manual, Locking Reads
https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html - MySQL 8.4 Reference Manual, InnoDB Locking
https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html - MySQL 8.4 Reference Manual, Phantom Rows
https://dev.mysql.com/doc/refman/8.4/en/innodb-next-key-locking.html - MySQL 8.4 Reference Manual, Undo Logs
https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html - MySQL 8.4 Reference Manual, Redo Log
https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html - MySQL 8.4 Reference Manual, Configuring Buffer Pool Flushing
https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool-flushing.html - MySQL 8.4 Reference Manual, InnoDB Checkpoints
https://dev.mysql.com/doc/refman/8.4/en/innodb-checkpoints.html
