좋아요 동시성 테스트를 하다가 "데드락"이 발생하는 걸 보았다. 이를 해결하기 위해 Transaction 격리 수준에 대한 개념을 제대로 잡고, 해결하고자 작성하는 글이다.
먼저, MySQL 을 사용하고 있어서 MySQL이 사용하는 락에 대해서 살펴보다가 동시성 제어를 위한 MVCC 의 등장 배경을 정리하고자 한다.
고전적인 락
MVCC 등장 이전에 공유락(S-lock), 배타락(X-lock)으로 동시성 제어를 하고 있었다. 하지만 이 방식은 데이터 일관성을 보장해주지만 너무 느리다는 단점이 있다.
공유락(Shared Lock)
나 이 데이터 읽을 거예요.
다른 읽는 트랜잭션? 오케이, 같이 읽자.
근데 누가 수정하려면, 내가 다 읽고 나서 해 주세요.
공유락은 위와 같이 읽기 잠금이다. 내가 다 읽을 때까지 동일한 데이터에 쓰기 작업을 하려는 다른 트랜잭션은 접근하지 못하도록 막는것이다.
- 1번 트랜잭션이 공유락을 가져갔다
- 2번 트랜잭션이 데이터를 읽는 경우, 1번 트랜잭션이 읽는 데이터에 변경이 없으므로 다른 공유락을 가져가서 동시 처리
- 2번 트랜잭션이 데이터를 쓰는 경우, 1번 트랜잭션이 읽는 데이터에 변경이 생길 수 있으므로 lock
배타락(Exclusive Lock)
나 이 데이터 수정할 거예요.
읽든 쓰든, 지금은 나만 접근해요. 끝나고 나면 당신들 마음껏 하세요.
배타락은 쓰기 잠금이다. 배타락은 지금 내가 이 데이터 읽을테니 아무도 방해하지 못하도록 막는 것이다.
- 1번 트랜잭션이 배타락을 가져갔다
- 2번 트랜잭션이 데이터를 읽는 경우, 1번 트랜잭션이 쓰는 작업을 하면서 데이터가 변경될 수 있으므로 lock
- 2번 트랜잭션이 데이터를 쓰는 경우, 1번 트랜잭션이 쓰는 작업을 하면서 데이터가 변경될 수 있으므로 lock
위와 같은 락은 동시성 저하가 심하다.
다른 트랜잭션이 읽기 / 쓰기 작업이 끝나야지만 해당 데이터에 접근할 수 있다.
MVCC 등장
MVCC: Multi Version Concurrency Control, 다중 버전 동시성 제어
동시 접근을 허용하는 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법 중 하나이다.
원본 데이터와 변경 중인 데이터를 동시에 유지 → snapshot을 활용해서 원본 데이터를 백업한다.
데이터를 여러 버전 관리할 수 있다는 말이 된다.
1번 트랜잭션이 데이터를 변경하면 변경된 데이터는 바로 실제 레코드에 반영되고, 원본 데이터는 Undo 영역에 보관해 둔다.
이게 동시성 제어랑 무슨 상관인가?
락의 수준을 제어할 수 있다는 점이다.
락 수준을 상황에 맞게 제어해서 동시성으로 인한 성능 저하를 피할 수 있게 된다.
고전적인 락에서는 select 절에서부터 락을 걸고 들어갔다. 단순히 읽기하는 트랜잭션인데 다른 트랜잭션이 접근을 못했다면, MVCC는 select에서 락을 걸지 않으니 다른 트랜잭션이 접근할 수 있다.
MVCC 특징
- 일반적인 RDBMS보다 매우 빠르게 작동
- 사용하지 않는 데이터가 계속 쌓이게 되므로 데이터를 정리하는 시스템이 필요
- 데이터 버전이 충돌하면 애플리케이션 영역에서 이런한 문제를 해결해야함
MySQL에서의 MVCC
RDBMS마다 MVCC를 지원하는 방식이 다르다.
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, "MangKyu", "서울");
데이터는 다음과 같은 상태로 저장된다. 메모리와 디스크에 모두 해당 데이터가 동일하게 저장된다.
그리고 UPDATE 문을 실행했다고 하면
UPDATE member SET area = "경기" WHERE id = 1;
UPDATE 문이 실행된 결과를 그림으로 표현하면 다음과 같다. commit 실행 여부와 무관하게 InnoDB 버퍼출은 새로운 값으로 갱신된다. 그리고 Undo 로그에는 변경 전의 값들만 복사된다. 그리고 InnoDB 버퍼 풀의 내용은 백그라운드 쓰레드를 통해 디스크에 기록되는데, 디스크에도 반영되었는지 여부는 시점에 따라 다를 수 있으므로 '?'로 표시하였다.
여기서 트랜잭션 격리 수준에서 어떤 값이 읽히는 지 알아보자
트랜잭션의 다양한 격리 수준
READ_UNCOMMITTED: 버퍼 풀의 데이터를 읽어서 반환한다. (commit 이전에 변경된 값을 읽는다.)
READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE: 변경되기 이전 값인 Undo 로그의 값을 반환한다.
여기서 Undo Log 영역의 데이터는 커밋 혹은 롤백을 호출하여, 더 이상 Undo 영역이 필요로 하는 트랜잭션이 없을 때 삭제된다.
MVCC가 등장 한 후 트랜잭션의 다양한 격리 수준이 나왔다고 볼 수도 있겠다.
ref