InnoDB는 각각의 레코드의 여러 버전(수정되기 전 버전들과 수정된 후의 버전)을 언두 로그 형태로 갖고 있는다.
클라이언트는 트랜잭션 동안에는 트랜잭션 시작 전에 커밋된 레코드, 그리고 그 이후의 변경된 버전만 보게 된다.(그 트랜잭션 내에서)
트랜잭션 레벨은 4가지가 있다.
READ UNCOMMITTED
커밋되지 않은 데이터도 읽을 수 있는 상태다.
CF) A 트랜잭션에서 처리한 작업이 완료되지 않았음에도, 다른 트랜잭션에서 변경된 내용을 볼 수 있는 문제를 '더티 리드'문제라고 한다. A트랜잭션에서 롤백을 했음에도, B 트랜잭션이 그 전에 데이터를 조회해서 작업을 진행했다면 데이터 일관성에 문제가 생길 수 있다.
READ COMMITTED
커밋된 데이터만 읽을 수 있다. 하지만, 한 트랜잭션 안에서 여러번 DB조회를 하면 그때마다 데이터가 달라지는 문제가 발생한다.
CF) 더티리드는 발생하지 않지만 NON_REPEATABLE READ라는 부정합 문제가 존재한다.
A트랜잭션에서 변경한 내용은 테이블에 기록되고, 이전 값은 언두로그에 저장된다. A트랜잭션이 커밋되기 전 그 데이터를 조회한 B트랜잭션은 언두 로그에서 값을 조회하는 것이다.
이 수준에서는
NON-REPEATABLE READ가 발생할 수 있다. 하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 REPEATABLE READ 정합성에 어긋나는 것을 말한다.
처음에 물건 이름이 AB라는 것을 조회했을 때, 일치하는 데이터가 없었다고 해보자. 그런데 다른 트랜잭션에서 AB라는 데이터를 저장했다. 그러면 그 이후에 AB를 조회했을 때 데이터가 조회된다.
하나의 트랜잭션에서 조회 결과가 달라질 수 있는 문제인 것이다.
REPEATABLE READ
한 트랜잭션 안에서 여러번 DB 조회를 해도 같은 데이터를 보장한다.
이 수준에서는 언두 영역에 백업된 데이터를 통해서, 트랜잭션 내에서 동일한 결과를 보여주도록 보장한다(트랜잭션이 시작되기 이전의 최신 커밋을 기점으로 보여주면 된다)
CF)
이 수준에서는 PHANTOM READ가 발생할 수 있다. SELECT ... FOR UPDATE 쿼리와 같은 쓰기 잠금을 거는 경우, 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상을 말한다.
A트랜잭션에서 상품 테이블에 INSERT하기 전과 후에, B트랜잭션에서 SELECT ... FOR UPDATE를 조회했다고 해보자.
이때, SELECT ... FOR UPDATE는 레코드에 잠금을 걸어야 하는데 언두 로그에는 잠금을 걸 수 없다고 한다. 그래서, SELECT ... FOR UPDATE 나 SELECT ... LOCK IN SHARE MODE 로 조회되는 레코드는 언두 영역의 변경 전 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져온다.
InnoDB 스토리지 엔진은 레코드 락과 갭 락을 합친 넥스트 키 락을 사용한다.
t 테이블에 c1 = 13 , c = 17 인 두 레코드가 있다고 가정하자.
이때 SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE 쿼리를 수행하면, 10 <= c1 <= 12, 14 <= c1 <= 16, 18 <= c1 <= 20 인 영역은 전부 갭 락에 의해 락이 걸려서 해당 영역에 레코드를 삽입할 수 없다.
또한 c = 13, c = 17인 영역도 레코드 락에 의해 해당 영역에 레코드를 삽입할 수 없다. INSERT 외에 UPDATE, DELETE 쿼리도 마찬가지이다.
이러한 방식으로 InnoDB 스토리지 엔진은 넥스트 키 락을 이용하여 PHANTOM READ 문제를 해결한다.
SERIALIZABLE
읽기 작업도 공유락을 획득하는 가장 엄격한 상태다.
레코드 하나에 대해서 읽기와 쓰기가 동시에 수행되면, 한 작업은 다른 작업을 계속 기다려야 한다. Lock없이 일관된 읽기를 제공하기 위해서 언두로그를 사용한다.
MVCC는 데이터를 읽을 때 특정 시점을 기준으로 가장 최근에 커밋된 데이터만 읽는다. 이를 이를 consistent read라고 한다.
MVCC는 데이터 변화 이력을 관리하는데, 그래서 추가적인 저장공간을 많이 사용한다. 대신 read와 write는 서로 block하지 않기 때문에 Lock-based concurrency control에 비해 더 뛰어난 performance를 낼 수 있다.
테이블 A에서 company 칼럼의 값이 Toss인 레코드르 생성한 후 커밋해보자. 그리고, Company를 TossPayments로 업데이트하고 커밋은 하지 않는다.
이 상황에서 다른 사용자가 select * from tableA 을 한 경우
READ UNCOMMITTED : ‘TossPayments’ -->커밋, 롤백과 상관없이 더티 리드(다른 트랜잭션에서 완료되지 않은 작업을 조회)
READ COMMITTED : ‘Toss’
REPEATABLE READ : ‘Toss’ -->
a가 select를 한다(트랜잭션 id =1) b가 업데이트를 한다(트랜잭션 id=2)-->a는 이후로 조회할 때 undo log에서 이전의 값을 가져온다.
이렇게 값이 조회된다.
격리 수준은 한 트랜잭션 내에서 데이터가 일관성 있게 보여지는 수준을 정의 한 것이다. 이를 위해서 undo log 를 이용한다.
MVCC가 없는 상황-재고 확인과 재고 수정이 동시에 일어남
직원 A: "1번 상품의 재고를 확인하고 있어요" (SELECT)
직원 B: "잠깐만요! 저도 1번 상품의 재고를 수정해야 해요" (UPDATE)
직원 A: "제가 다 볼때까지 기다리세요..."
직원 B: "네... 기다릴게요..." (답답)
MVCC가 있는 상황-같은 데이터에 대해 다른 버전을 보면서 작업
직원 A: "1번 상품 재고가 100개네요" (이전 버전 확인)
직원 B: "저는 재고를 80개로 수정할게요" (새 버전 생성)
결과:
직원 A는 계속 100이라는 숫자를 보면서 작업
직원 B는 기다리지 않고 80으로 바로 수정
서로 방해하지 않고 각자 작업 가능
MVCC는 읽기 전용 트랜잭션과 쓰기 트랜잭션간의 충돌을 막기 위해서 사용한다. 읽기 트랜잭션은 여러 버전의 레코드를 읽고, 쓰기 트랜잭션은 새로운 버전을 추가하기만 하면 된다.
언두로그와 성능
그렇다면, 언두로그는 성능에 어떻게 영향을 미칠까?
1)언두로그도 디스크에 저장되기에 디스크 공간을 많이 쓰고, 나중에 커밋할 때 I/O가 오래걸린다.
2)언두로그는 롤백시 빠르게 접근될 수 있도록 메모리에도 저장되는데, 커질 수록 많은 메모리를 잡아먹는다.
3)언두로그가 많아질수록, 더 많은 언두로그가 사용된다. 이는 언두로그에 대한 경합과 락 대기 발생으로 이어질 수 있다.
4)롤백에서 더 많은 시간이 걸린다. 언두로그를 더 많이 읽어야 하기 때문이다.
참고자료