새소식

인기 검색어

개발일기

Isolation Level과 MVCC

  • -

Isolation Level과 MVCC

여태 한 트랜잭션 내에서 SELECT문을 실행하면 s-lock을 거는줄 알았다. 그 이유는, ACID 특성 중, Isolation에 의해 다른 트랜잭션에서 수정한 결과가 현재 트랜잭션에 영향을 미쳐선 안된다고 생각했기 때문이다.

 

그러나, InnoDB로 실제로 실행해본 결과 s-lock을 걸지 않음을 알 수 있었다. 그럼에도 불구하고 다른 트랜잭션에서 수정한 결과가 현재 트랜잭션에 반영되지 않음을 볼 수 있었다. 그 이유는 뭘까?

1 ~ 8까지 순서대로 실행한 결과 s-lock이 걸리지 않았음에도 3과 5는 홍철 없는 홍철팀, 6에선 홍철팀을 반환한다.

바로 그건 트랜잭션 격리 수준(Isolation Level) 때문이다. InnoDB의 default isolation level은 REPEATABLE_READ이다. 때문에 다른 커넥션에서 수정한 결과가 현재 커넥션에 간섭할 수 없다.

 

그렇다면 격리 수준을 READ_COMMITED로 낮추면 어떻게 될까? 이번에는 수정된 데이터를 읽을 수도 있고, 아닐 수도 있다. 이 이유에 대해 이해하려면 각 트랜잭션 격리 수준에서 MVCC가 어떻게 동작하는지 이해해야 한다.

 

MVCC란 Multi-Version Concurrency Control을 의미한다. 레코드에 대해 수정사항이 발생하면 복사본을 만들어 원본은 undo log에 저장하고 commit하면 현재 수정된 내용(InnoDB 버퍼풀)을 저장하고, rollback하면 undo log에 있던 원본을 저장한다.

 

읽을 때는 레코드를 수정한 커넥션이라면 InnoDB 버퍼풀의 레코드를 반환하고, 다른 커넥션이라면 원본인 undo log에 있는 레코드를 반환한다. 이러한 특성 때문에 READ_COMMITED는 COMMIT 이전의 데이터는 읽지 않는다.

 

 

MVCC의 가장 큰 목적은 잠금을 사용하지 않고 일관된 읽기를 제공한다는 점이다. 그러나, 트랜잭션이 길어지면 undo 영역에서 관리해야 하는 버전의 데이터들이 많아지며 성능에 영향을 줄 수 있기 때문에 주의해야 한다.

 

lock은 실제로 변경되는 것을 잠가 막는 개념이고, versioning은 변경을 보지 못하도록 하는 개념이라고 생각할 수 있다.

 

READ_COMMITED란 commit된 상태라면 그것이 원본이라 생각하고 읽겠다는 의미이다. 따라서 요청이 commit되면 다른 커넥션에서 해당 레코드에 대해서 undo 영역이 아닌 수정된 원본을 읽는다.

 

이럴 경우 

 

REPEATABLE_READ란 한 커넥션에서 같은 데이터를 여럿 읽는다면 같은 값을 반환받는 걸 보장받겠다는 의미이다.

REPEATABLE_READ와 READ_COMMITED의 차이는 언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가야 하느냐에 있다.

 

모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호를 가지며, 이는 순차적으로 증가한다. 따라서, 오래된 트랜잭션일 수록 더 작은 숫자를 가진다.

 

undo 영역에 백업된 모든 레코드에는 변경을 발생시킨 트랜잭션 번호가 포함되어 있다. 따라서 이를 보고 커넥션 별로 어떤 데이터를 읽어야 할지 판단할 수 있다. 아마 현재 trx_id보다 작은 수중 가장 큰 수가 대상이 될 것이다.

 

 

REPEATABLE_READ에서도 Phantom Read라는 문제점이 발생한다. 이 현상은 undo 영역이 아닌 현재 버퍼풀에 들어있는 레코드 값을 읽을 때 발생한다.

 

예를 들어 SELECT ... FOR UPDATE와 같은 쿼리가 있다. 이 쿼리는 x-lock을 거는 쿼리인데, undo 영역에는 lock을 걸 수 없기 때문에 현재 레코드를 읽어오게 된다.

 

앞서 언급했듯 버저닝이란 디스크에 있는 원본은 변경되고, undo에 있는 데이터를 읽어 변경을 보지 못하게 하는 전략이다. 그런데 락을 걸려면 어쩔 수 없이 버저닝을 사용하지 못하게 된다.

 

따라서 현재 트랜잭션 이후에 추가된 데이터라 할지라도, 보였다 안 보였다 하는 현상이 발생할 수 있게 되는 것이다.

 

이 문제를 해결하기 위해 REPEATABLE_READ에서는 Gap Lock을 지원한다. Gap Lock은 범위 쿼리 발생시 해당 범위(Gap)에 대해 모두 잠금을 거는 행위이다.

 

모두 잠금이 걸리기 때문에 Phantom Read는 일부 회피할 수 있게 되나, 잠금이 많아지기 때문에 Dead Lock에 취약해진다는 단점을 가지고 있다.

마치며

SERIALIZABLE과 READ_UNCOMMITED + Dirty Read라는 키워드들도 있으나, 이번 포스팅에선 따로 다루지 않았다. MVCC와 관련이 있는 주제는 아니라고 생각했기 때문이다.

 

또한 배운 점은, 이미 알고 있다고 생각한 개념인데 그냥 외워서 알고 있는 느낌이 강했다는 생각이 든다. 또한 잘못 알고 있었던 개념들도 많았다. 이번 기회에 내가 안다고 생각했던 사실들에 대해 잘 모르고 있었을 수도 있겠다는 생각이 많이 들었다. 이래서 벼는 익을수록 고개를 숙이게 된다고 하나보다.

 

중요한 것은 사용중인 트랜잭션 격리 수준에 의해 실행하는 SQL 문장이 어떤 결과를 가져오게 되는지를 정확히 예측할 수 있어야 한다는 것이다. 그리고 당연히 이를 위해서는 각 트랜잭션 격리 수준이 어떻게 작동하는지 알아야 한다.

- Real MySQL

 

참조

- Real MySQL

- MySQL 공식문서 - 15.7.2.1 Transaction Isolation Levels : https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html

- 테코블 - 트랜잭션 격리 수준 : https://tecoble.techcourse.co.kr/post/2022-11-07-mysql-isolation/

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.