Real My SQL - 4장 트랜잭션

김인회·2021년 6월 26일
0

Real My SQL

목록 보기
3/4

4장 트랜잭션과 잠금

해당 파트에서는 MySQL에서의 트랜잭션과 잠금에 대한 구체적 내용을 다룬다.

나는 해당 파트의 여러 세부적인 내용 중에서도

데이터베이스에서 트랜잭션의 고립(격리) 정도를 왜 구별하여 나누어 놨는지,

각 고립 수준 별로 어떠한 차이가 존재하는지,

MySQL 특히 그중 InnoDB 에서는 어떠한 잠금 전략들을 취하고 있는지와 같은 내용들을

중점적으로 보았고 이러한 부분을 정리하고자 한다.

InnoDB의 Lock

InnoDB는 기본적으로 레코드 수준의 잠금, 즉 row-level의 Lock을 이용한다.

이 Record Lock은 크게 2가지 종류로 또 나눠 볼 수 있는데 바로 읽기잠금 이라고도 부르는 Shared Lock 과 쓰기잠금 으로 불리는 Exclusive Lock 이다.

(처음 읽기잠금(Shared Lock)이라는 용어를 들었을 때는, 읽기잠금이면 다른 트랜잭션들이 데이터를 아예 읽어가지 못하는 식으로 잠그는 것을 말하는 건가라고 생각했었는데 딱히 그런 것은 아니었다.)

Shared Lock 은 한 단어로 표현하자면 Read-only 수준의 잠금이라고 말할 수 있다.

특정 레코드에 대한 Shared Lock은 여러 트랜잭션에서 공유하며 획득할 수 있는데, 만약 특정 레코드의 Shared Lock을 가진 트랜잭션들이 현재 존재하고 있는 상황이라면, 이 트랜잭션들의 작업이 전부 다 끝난 뒤에야 그 레코드에 대한 상위 잠금인 Exclusive Lock을 획득할 수 있는 구조다.

어떻게 보면 Shared Lock은 Exclusive Lock을 걸지 못하도록 하는 잠금이라고도 볼 수 있겠다.

Exclusive Lock 은 쓰기를 위한 잠금, 즉 일종의 쓰기 권한이라고 볼 수 있겠다.

특정 레코드에 대한 변경 작업을 수행하기 전에는 반드시 이 Exclusive Lock을 먼저 획득해야지만 변경 작업을 진행할 수 있다.

해당 잠금은 Shared Lock 처럼 여러 트랜잭션들이 중복해서 공유할 수 없으며 오로지 하나의 트랜잭션만이 해당 잠금을 독점해서 사용하는 구조이기 때문에, 이 잠금을 이용하면 어떠한 충돌 없이 레코드를 안정성 있게 변경할 수 있게 된다.

이 2가지 잠금 개념이 바로 InnoDB가 가지고 있는 기본적인 잠금 개념이다.

(이 외에도 레코드 수준이 아닌 테이블 수준의 잠금인 Intention lock이 존재한다. 따라서 실질적으로 InnoDB의 Lock은 IS, IX, S, X lock 총 4가지로 분류된다.)

그리고 InnoDB는 이러한 기본적인 Lock 개념을 이용하여 여러 가지 복합적인 잠금 기법을 구현했는데, 이러한 잠금 기법에는 Gap Lock, Nest Key Lock, Insert Intention Lock, Auto Increment Lock 등과 같은 것들이 존재한다.

인덱스 기반의 잠금

InnoDB의 레코드 기반 잠금 방식에는 다른 DBMS의 레코드 락과는 다른 특이한 점이 존재한다.

바로 작업 대상으로 선정된 레코드 그 자체를 잠그는 것뿐만이 아니라, 대상 레코드를 찾기 위해서 탐색 작업을 하는 도중에 발견되었던 모든 레코드를 잠근다는 것이다.

조금 더 정확히 말하자면 InnoDB는 작업 대상으로 지정된 레코드를 찾기 위해 인덱스라는 것을 이용하여 탐색 범위를 좁히며 효율적으로 탐색을 진행하고자 하는데, 이때 대상과 관련된 인덱스 범위에 해당하는 레코드들을 모두 잠궈버리는 특이한 방식을 취하고 있다.

(인덱스는 InnoDB에서 빠른 데이터 탐색을 위해 테이블에 미리 설정해놓은 정렬이 되어있는 데이터들이다.)

즉 InnoDB는 특이하게도 인덱스 기반의 레코드 잠금 방식을 이용하고 있다.

InnoDB가 인덱스 기반의 레코드 잠금 방식을 택한 그 특별한 이유가 무엇인지는 사실 잘 모르겠다.

(안정성 있는 복제 기능을 구현하기 위하여 InnoDB는 Next Key Lock 잠금 방식을 이용하는데 이것이 결국 인덱스 기반 잠금의 확장이기 때문에?)

하지만 어쨌든 이러한 인덱스 기반의 레코드 잠금 방식에 Next Key Lock 잠금 방식이 확장되면서 InnoDB는 특별하게도 다른 SQL DB와는 다르게 Repeatable-read 고립 수준에서 Phantom read의 영향을 받지 않고 있다.

Next Key Lock

위에서도 먼저 언급했듯이 Next Key Lock은 InnoDB의 인덱스 기반 잠금을 확장한 것이라고 볼 수 있다.

InnoDB는 단순히 작업 대상의 레코드 자체를 잠그는 것뿐만이 아니라, 해당 대상을 찾기 위해서 이용한 인덱스들의 레코드를 모두 잠근다.

그리고 인덱스들의 레코드뿐만이 아니라 여기서 추가적으로 각 레코드 사이의 간격마저도 잠궈버린 것이 바로 Next Key Lock이다.

즉 인덱스 기반으로 잠가진 레코드들 사이에 추가적으로 데이터가 Insert 되는 것을 방지하는 것(Gap Lock)이 바로 Next Key Lock 이다.

트랜잭션의 격리 수준

트랜잭션의 격리 수준은 동시에 존재하는 여러 트랜잭션들이 서로 얼마만큼 격리되어 있는지 그 수준을 구별하여 나타낸 것이다.

이전 글에서도 언급했듯이 여러 사용자가 동시다발적으로 이용하고 있는 데이터베이스는 시시각각 데이터가 유동적으로 변하고 있는 중이기 때문에 그저 단순한 정적 데이터들의 모임으로 바라볼 수 없다.

현재 내가 열람하고 있는 데이터는 해당 순간에도 다른 사용자에 의해 변경되고 있는 중일 수도 있다는 것이다.

이렇게 데이터가 유동적으로 바뀌는 상황은 트랜잭션의 일관성을 떨어트린다. (Dirty Read, Non-Repeatable Read, Phanthom Read)

하나의 트랜잭션 내에서 완전히 같은 내용의 쿼리를 날리더라도 매 시각 변하는 데이터로 인해 전혀 다른 결과값을 리턴 받게 될 수도 있는 것이다.

이러한 트랜잭션 내의 일관성 문제를 바로 트랜잭션의 격리 수준을 통해 조절할 수 있다.

데이터에 적절하게 잠금을 걸어 특정 시점에 데이터가 변하지 않도록 막거나 혹은 변하더라도 적용되지 않도록 막는 식으로 트랜잭션들을 서로 격리하고 트랜잭션의 일관성을 조절하는 느낌이다.

가장 높은 격리 수준인 Serializable 수준을 예시로 들어보면, 해당 격리 수준에서는 누군가 특정 데이터에 대한 읽기나 쓰기 작업을 먼저 진행하였다면 해당 데이터에 Lock을 걸어 이후의 사용자들은 Lock이 풀릴 때까지 데이터에 접근 하지 못하도록 막는다.

Serializable 수준의 격리 단계에서는 결국 데이터에 먼저 접근한 트랜잭션이 데이터를 선점하고 작업이 모두 완료된 이후에나 Lock을 해제하게 되므로, 해당 고립 수준에서 각 트랜잭션들은 자신의 작업 도중 관련 데이터가 변동되는 것과 같은 비일관적인 리턴을 경험하게 될 일이 없다.

Serializable 고립 수준은 위와 같이 꼼꼼한 Lock을 통해 트랜잭션의 안정적인 일관성을 구현하였지만 그로 인해 동시성(성능)을 희생시켰다는 단점이 존재한다.

트랜잭션 내에서 일관성과 동시성은 결국 반비례적인 관계로 한쪽을 취하게 되면 다른 한쪽은 포기하게 될 수밖에 없는 구조이다.

양쪽 중 어느 쪽을 취할 것인가, 어느 정도 수준으로 트랜잭션을 고립시킬 것인가 그 비율을 조절하는 것이 바로 고립 수준을 설정하는 것이라고 볼 수 있다.

일반적으로 DBMS에서 트랜잭션의 격리 수준이 낮아질수록 잠금은 더 느슨해지고, 그로 인해 일관성을 잃게 되는 대신 더 큰 동시성(성능)을 얻게 된다.

그런데 잠금 없는 읽기를 지향하고 있는 InnoDB에서는, Serializable 격리 수준을 제외하면 그 아래 단계의 트랜잭션 격리 수준들은 놀랍게도 사실상 모두 동일한 수준의 잠금을 취하고 있다.

InnoDB는 Undo 로그를 활용하여 데이터의 다중 버전을 보유하고 있기 때문에, 작업 중인 데이터에 굳이 잠금이라는 값비싼 비용을 치르지 않더라도 트랜잭션의 일관성을 조절할 수가 있기 때문이다.

예를 들어, 만약 InnoDB가 Read Uncommitted 격리 수준으로 설정되어 있다면 그냥 현재 버퍼풀에 저장되어 있는 가장 최신의 데이터를 가져올 것이고, Read Committed 설정 수준이라면 가장 최근의 Undo 로그 데이터를 가져오게 될 것이다(커밋 전 가장 최근의 Undo로그, 커밋 이후 버퍼풀 데이터).

만약 Repeatable Read 수준이라면 Undo 로그에서 자신보다 늦게 태어난 트랜잭션들의 데이터 버전은 건너 뛰어가며 가장 알맞은 버전의 데이터를 가져오는 식으로 작동할 것인데, 즉 트랜잭션이 생성된 그 시점 이후에 변동된 데이터의 기록들은 가려지게 된다.

따라서 잠금 없이도 하나의 트랜잭션에서 똑같은 쿼리 문장은 항상 같은 리턴 값을 보장받는(Repeatable Read) 일관성을 유지할 수 있게 되는 것이다.

더 알아둘만한 것

  1. 일반적인 DBMS의 Repeatable Read 격리 수준에서 select ... for update 같은 쿼리를 진행할 경우 (쓰기 권환을 획득하며 데이터를 조회할 때) Phanthom Read가 발생할 확률이 생기는데 InnoDB에서는 Next Key Lock으로 해당 범위에 데이터의 Insert 자체가 아예 막혀 버려 Phanthom Read가 발생하지 않는다.
profile
안녕하세요. 잘부탁드립니다.

0개의 댓글