MySQL

트랜잭션과 잠금

iksadnorth 2023. 8. 2. 13:33

해당 게시물은 'Real MySQL 8.0'라는 책을 참고해서 작성했습니다.

👣 트랜잭션

'쪼갤 수 없는 업무 처리의 단계'로 정의될 수 있다.
트랜잭션은 ACID라고 불리는 4 가지 특성을 가지고 있다. 
해당 특성을 요약하면 결국 데이터의 정합성을 보장하는 특성들이라고 볼 수 있다.

MySQL의 스토리지 엔진 중 InnoDB는 트랜잭션을 지원하고 MyISAM은 트랜잭션을 지원하지 않는다.

 

👣 잠금

동시성을 구현하기 위해 트랜잭션들이 무분별하게 특정 데이터를 사용하는 것을 막아야 한다.
이를 위해 잠금이라는 기능을 이용하는데 데이터에 대한 접근을 특정 트랜잭션에게만 허용함으로서 
데이터의 정합성을 실현할 수 있다.

👣 MySQL 엔진의 Lock

MySQL 엔진은 모든 스토리지 엔진에 영향을 미친다.

MySQL 엔진 잠금에는 4가지의 종류가 있다.

1. 글로벌 Lock
2. 테이블 Lock
3. 네임드 Lock
4. 메타데이터 Lock

👣 글로벌 Lock

말그대로 특정 트랜잭션의 글로벌 락이 풀리기 전까지 모든 것을 잠그는 방법이다.
다른 테이블, 다른 데이터베이스에도 영향을 미치는 범위로서 그냥 서버 전체에 대한 락이라고 봐도 무방하다.

MyISAM이나 MEMORY 테이블에 대해 mysqldump로 일관된 백업을 받아야 할 때 사용한다.

InnoDB 같은 경우엔 일관된 데이터 상태를 위해 굳이 사용하지 않아도 된다.

👣 테이블 Lock

개별 테이블 단위로 설정되는 잠금이다.
MyISAM이나 MEMORY 테이블에 데이터를 변경하는 쿼리를 실행하면 발생한다.

역시나 InnoDB의 DML 쿼리의 경우엔 레코드 단위의 락을 수행하기 때문에 굳이 사용하지 않아도 된다.
하지만 DDL 쿼리는 테이블 락이 설정된다.

👣 네임드 Lock

GET_LOCK()함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다.
자주 사용되지 않는 방식이며, DB 서버 1대에 5대의 웹 서버가 접속해서
서비스 하는 상황처럼 동기화 과정이 중요한 경우 네임드 락을 이용해 쉽게 처리 가능하다.

👣 메타데이터 Lock

데이터 베이스 객체[테이블, 뷰 등등]의 이름이나 구조를 변경하는 경우에 획득하는 잠금.

 

👣 InnoDB 스토리지 엔진의 Lock

스토리지 엔진 레벨의 잠금은 다른 스토리지 엔진에 영향을 주지 않고 오로지 해당 스토리지 엔진에게만 영향을 준다.

InnoDB 스토리지 엔진의 경우 레코드 기반 잠금 방식을 탑재하고 있기에 MyISAM보다 뛰어난 동시성 처리를 제공한다.

InnoDB는 다른 DBMS와 다르게 레코드락 이외의 락도 제공한다.

1. 레코드락
다른 DBMS와 다르게 레코드 자체에 대한 락이 아니라 인덱스의 레코드를 잠근다.

2. 갭 락
레코드 자체가 아니라 레코드와 인접한 레코드 사이를 잠근다.
실제로 잠그는 것은 INSERT를 통한 생성을 제언한다는 의미다.

3. 넥스트 키 락
레코드 락과 갭 락의 합쳐진 형태.
넥스트 키 락은 바이너리 로그에 기록되는 쿼리가 리플리카 서버에서 실행될 때
소스 서버에서 만들어낸 결과와 동일한 결과를 만들어내도록 보장해주는 것이 주목적이다.
그런데 의외로 넥스트 키 락과 갭 락으로 인해 데드락이 발생하거나 다른 트랜잭션이 기다리는 일이 자주 발생하므로,
바이너리 로그 포맷을 ROW 형태로 바꿔서 넥스트 키 락이나 갭 락을 줄이는 것이 좋다고 한다.

4. 자동 증가 락
MySQL에서는 레코드를 구분하기 위해 일련번호 값을 부여하는데
이 값을 위해 AUTO_INCREMENT라는 칼럼을 사용한다.
이것은 순차적으로 증가하는 특성이 있는데
만약 INSERT 문이 동시에 호출되어 순차적으로 증가하는 값을 가져가는 것이 아니라
같은 가져가게 되면 큰 낭패를 볼 수 있다. 때문에 AUTO_INCREMENT 락을 통해 이런 혼동을 막아낸다.

👣 MySQL의 격리 수준

트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다. 총 4가지 수준이 존재한다.

1. READ UNCOMMITTED
2. READ COMMITTED
3. REPEATABLE READ
4. SERIALIZABLE

또한 해당 격리 수준에서의 부정합 종류는 3가지가 존재한다.

1. DIRTY READ
2. NON-REPEATABLE READ
3. PHANTOM READ

위 조합들은 아래와 같이 표현 가능하다.

  DIRTY READ NON-REPEATABLE READ PHANTOM READ
READ UNCOMMITTED O O O
READ COMMITTED   O O
REPEATABLE READ     O (InnoDB는 없음.)
SERIALIZABLE      

👣 READ UNCOMMITTED

트랜잭션 A가 커밋을 하지 않아도 트랜잭션 B가 열람할 수 있는 수준.

👣 READ COMMITTED

트랜잭션 A가 커밋하지 않으면 트랜잭션 B가 열람할 수 없는 수준.
InnoDB에서는 Undo Log로 이것을 실현한다.
트랜잭션 A는 실제로 Buffer Pool의 데이터를 수정하긴 하지만
트랜잭션 B는 미리 Undo Log에 백업한 데이터를 참고해서 연산하기 때문에 
정합성에 문제가 없다.

하지만 이는 Unrepeatable Read를 유발할 수 있다.
트랜잭션 A가 시작하고 트랜잭션 B 모든 처리를 끝낸 후, 트랜잭션 A가 끝낸다면,
트랜잭션 A는 트랜잭션 B의 이전에는 변경 전의 데이터를 참고하지만
트랜잭션 B의 이후에는 변경 후의 데이터를 참고하게 된다.
이는 데이터 정합성을 해치게 된다.

👣 REPEATABLE READ

해당 격리 수준이 InnoDB에서 사용하는 격리 수준이다.

InnoDB에서는 각 트랜잭션 마다 고유한 트랜잭션 번호를 가지고 있고
이 번호는 순차적으로 증가하는 값을 가지고 있다.
Undo Log에 데이터를 백업할 때, 항상 트랜잭션 번호를 포함해서 저장하기 때문에 
트랜잭션 A[ID: 6]가 참고하려는 Undo Log의 값의 ID보다 작으면 사용해도 되고 
그렇지 않다면 사용하지 않음으로서 Repeatable Read를 실현할 수 있다.

하지만 이는 Phantom Read를 유발할 수 있다.
예를 들어 "Select * From Table Where age >= 10"라는 쿼리를 트랜잭션 A 초기에 수행했다고 가정하자.
해당 쿼리 이후에 트랜잭션 B가 age=15인 레코드를 추가로 삽입하고 커밋한다.
그 이후 또다시 "Select * From Table Where age >= 10"를 호출하면
기존의 결과보다 1개 더 많은 값을 내놓게 되면서 정합성이 깨진다.
이는 Transaction ID로도 Undo Log로도 해결하지 못한다.

👣 SERIALIZABLE

단순하게 모든 트랜잭션은 서로 병렬적으로 수행할 수 없다는 격리 수준이다.
모든 트랜잭션이 직렬적으로 수행되기 때문에 매우 성능이 떨어진다.
하지만 모든 정합성은 지켜질 수 있다.

하지만 InnoDB에서는 넥스트 키 락 덕분에 이미 Phantom Read가 발생하지 않기 때문에 
굳이 사용할 이유는 없다.
넥스트 락 키가 Phantom Read를 방지하는 이유는 다음과 같다.

  1. 넥스트 락 키는 범위 검색의 끝점까지 락을 걸기 때문에,
    해당 범위 내에서 새로운 레코드가 추가되거나 삭제되더라도 락이 걸려 있기 때문에
    다른 트랜잭션이 해당 레코드를 변경할 수 없다. 따라서 판텀 리드가 발생하지 않는다.
  2. 범위 검색을 수행할 때 락을 거는 것은 검색 범위의 경계 값을 포함하여 락을 걸기 때문에,
    레코드 사이에 빈 공간이 있더라도 락이 걸린다.
    따라서 다른 트랜잭션이 새로운 레코드를 추가해도 해당 범위 내에서 락이 걸려있으므로
    판텀 리드가 발생하지 않는다.

'MySQL' 카테고리의 다른 글

옵티마이저, 힌트  (0) 2023.09.13
인덱스  (0) 2023.08.02
MySQL 로그 파일  (0) 2023.08.01
MyISAM 스토리지 엔진  (0) 2023.08.01
InnoDB 스토리지 엔진  (0) 2023.08.01