* 학습에 도움을 주신 스티디원 오길환님께 감사드립니다.
트랜잭션(Transaction)
위키피디아에 설명하는 데이터베이스 트랜잭션
- 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위입니다.
- 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미합니다.
이 말인 즉슨 클라이언트가 서버에 요청을 보낸다고 했을 때, 그 요청 자체를 하나의 트랜잭션이라고 볼 수 있다. 요청은 하나지만 요청을 받는 서버는 여러 단계를 거쳐 처리할 수 있고 그 과정에서 성공하거나, 실패할 수 있습니다.
이러한 트랜잭션이 가지는 특성은 다음과 같습니다.
트랜잭션의 특성
원자성(Atomicity): 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력입니다.
일관성(Consistency): 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 의미합니다.
격리성(Isolation): 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미합니다.
영구성(Durability): 한 번 저장된 트랜잭션을 영구히 보존됩니다.
위 특성 중 유심히 봐야하는 것은 일관성과 격리성입니다. 먼저 일관성과 일관성과 항시 따라다니는 동시성에 대하여 알아봅시다.
동시성과 일관성
트랜잭션의 특성 중 일관성이 완전히 보장될 경우 여러 클라이언트의 요청을 받는 데이터베이스의 특성상 응답의 지연이 발생하는, 다시 말해 동시성이 저해되는 현상이 발생할 수 있습니다.
극단적인 예로, 일관성이 너무 높으면 한 테이블에 접근 및 작업 해야 하는 수 많은 트랜잭션 중 단 하나 씩 만이 접근 및 작업이 가능해지는 상황이 발생 됩니다.
그렇다고 동시성을 너무 높여버리면, 흔히 말하는 데이터가 꼬이는 상황이 발생할 수 있으므로 운영 중인 서비스의 특성에 따라 적절한 균형을 조정하는 것이 필요합니다. 이러한 조정을 위한 방법 중 하나가 격리 레벨(Isolation Level) 이라고 볼 수 있습니다.
격리 레벨
격리 레벨은 흔히 네 단계로 구성되어 있습니다.
각 레벨의 한국어 해석을 이해하면 더 숙지하기 쉽습니다.
- Read Uncommitted: 커밋되지 않은 것을 읽는다.
- Read Committed: 커밋된 것을 읽는다.
- Repeatable Read: 반복적으로 읽는다.
- Serializable: 직렬화가 가능하다
격리 레벨이 Read Uncommitted 인 경우 동시성이 높아 여러 사용자가 같은 데이터를 읽고 수정할 수 있게 되는 반면 데이터 자체가 잘못될 수 있어 일관성이 떨어질 수 있습니다.
반면, Serializable는 직렬화가 가능하단 뜻처럼 테이블 데이터에 여러 트랜잭션이 줄서서 기다리듯 처리 되게 됩니다. 다시 말해 한 트랜잭션만 읽고 수정할 수 있게 되므로 일관성은 극도로 높아지지만, 동시성은 현저히 떨어지게 됩니다.
개별 격리 레벨의 특성으로 인해 발생하는 현상들 또한 다양합니다. 지금부터 MySQL 을 통해 실제 쿼리를 실행하며 테스트 해보는 시간을 가져 보겠습니다.
테스트 방법
도커로 MySQL 8.0 이미지를 다운로드 받아 컨테이너를 실행해 테스트를 진행했습니다.
테스트를 위해 사용할 테이블은 아래 SQL문으로 생성해주겠습니다.
create database test;
use test;
create table test (test integer);
create index test_pk on test(test);
insert into test values (1);
insert into test values (6);
insert into test values (11);
insert into test values (16);
insert into test values (21);
한 MySQL에 두 개의 터미널로 접속하여 아래 내용을 따라 치며 테스트 하시면 되겠습니다.
Read Uncommitted, 커밋되지 않은 데이터를 읽음
-- 해당 세션의 트랜잭션 격리 레벨을 설정하는 명령어이다.
set transaction_isolation = 'READ-UNCOMMITTED'
-- 다음 세션에도 해당 격리 레벨을 설정하고 싶으면 위 명령어에 global 키워드를 붙이면 된다.
set global transaction_isolation = 'READ-UNCOMMITTED'
-- 설정된 격리 레벨을 조회하는 명령어다.
show variables like 'transaction_isolation'
- 터미널 B가 2번 단계에서 조회한 데이터와 4번 단계에서 조회한 데이터가 다르다.
- 3번 단계에서 터미널 A가 삽입한 데이터가 커밋되지 않았음에도 불구하고, 4번 단계의 조회에서 출력된 것이다.
- 이 처럼, 한 트랜잭션에서 커밋되지 않은 데이터를 읽는 현상을 Dirty Read라고 부른다.
- 그 외에도, Non-Repeatable Read (반복적이지 않은 읽기), Phantom Read (없던 유렁 데이터가 생겨남) 현상도 발생한다.
Read Committed, 커밋된 데이터를 읽음
-- 해당 세션의 트랜잭션 격리 레벨을 설정하는 명령어이다.
set transaction_isolation = 'READ-COMMITTED'
- 커밋되지 않는 데이터를 읽는 더티 리드 현상은 2번 단계와 4번 단계의 조회 데이터가 동일한 것을 봤을 때 해결된 것을 알 수 있다.
- 하지만 4번, 6번 단계를 봤을 때 커밋 전, 후로 데이터가 다른 것을 알 수 있다.
- 말 그대로 commit된 데이터를 읽을 수 있는 격리 레벨이기 때문에 삽인된 데이터 31이 읽힌 것이라고 할 수 있다.
- 이 처럼, 한 트랜잭션에서 읽어지는 데이터가 반복적이지 않은 경우를 Non-Repeatable Read(반복적이지 않은 읽기) 라고 한다.
- 그 외에도 Phantom Read (없던 유령 데이터가 생겨남) 또한 발생한다.
(+) Read Committed가 Dirty Read를 막는 원리는?
- 기본적으로 데이터를 수정하면 원본 데이터는 Undo Segment에 저장되고, 수정데이터는 데이터 파일에 저장됩니다.
- Read Uncommitted는 데이터 파일에서 데이터를 읽어 옵니다.
- Read Committed는 데이터 파일이 커밋되지 않은 데이털 일 경우 Undo Segment의 최신 Undo Record를 읽어 옵니다.
Repeatable Read 반복적인 읽기
-- 해당 세션의 트랜잭션 격리 레벨을 설정하는 명령어이다.
set transaction_isolation = 'REPEATABLE-READ'
- Non-Repeatable Read 현상은 더 이상 발생하지 않는 것을 알 수 있습니다.
- 하지만 조회 시 for share, for update 구문을 붙이거나 다른 트랜잭션에서 삽입/수정한 row에 대해 update를 하면, 그 다음 select 때 없던 데이터가 생기는 Phantom Read(유령 데이터를 읽음) 현상을 확인할 수 있습니다.
Serializable 직렬화가 가능함.
-- 해당 세션의 트랜잭션 격리 레벨을 설정하는 명령어이다.
set transaction_isolation = 'SERIALIZABLE'
- 터미널 B가 조회를 하는 것만으로 공유 락이 걸립니다.
- 공유락이 걸렸을 때는 삽입, 수정, 삭제, for update 조회가 불가한 반면 for share 조회와 일반 조회는 가능 합니다.
- 상세한 내용은 아래에서 더 설명드리겠습니다.
- 조회만으로 락이 걸리는 만큼, 다른 격리 레벨에 비해 동시성이 현저히 떨어지는 것을 알 수 있습니다.
락
데이터의 일관성을 보장하기 위한 방법 중 하나로, 특정 데이터에 대해 여러 트랜잭션이 무분별하게 동시에 접근하는 것을 막는 장치라고 할 수 있습니다. 이 락은 공유 락과 배타 락 두 종류로 구분할 수 있습니다.
공유 락 (Shared Lock)
공유락은 아래와 같은 쿼리를 실행할 시 발생합니다.
- SELECT ... FOR .. FOR SHARE
- INSERT ... SELECT ... FROM ...
공유 락을 했을 때의 효과는 다음과 같습니다.
- 공유 락이 걸린 레코드는 UPDATE, SELECT FOR UPDATE, DELETE 불가능
- 다른 트랜잭션이 다른 공유 락을 거는 것은 가능.
- 갭 락으로 특정 레코드 범위에 INSERT가 불가능
- 테이블 락으로 테이블 구조 변경 불가능 (컬럼 추가/삭제/수정, 인덱스 추가 등)
- Serializable 격리 레벨일 때 SELECT를 하면 FOR SHARE가 활성화되고, 자연스럽게 공유 락이 걸리게 됩니다.
갭 락이란?
FOR SHARE 혹은 FOR UPDATE로 특정 범위에 대해 조회했을 때,
실제 인덱스 레코드를 잠그는 것 뿐만 아니라
그 조회 대상인 인덱스 레코드의 이전 인덱스 레코드부터, 다음 인덱스 레코드 이전까지의 범위에 대해 잠그는 것을 말합니다.
예를 들어 테이블에 데이터가 1, 6, 11, 16, 21 이렇게 있고 11부터 16까지의 범위로 조회를 하면, 6 ~ 20 까지의 범위가 갭락에 걸리게 됩니다.
.
이 갭락은 꼭 테이블 상에 데이터가 없어도 됩니다.
전체 데이터가 1, 6, 11, 16, 21 이 있을 때, 21 부터 30까지의 범위로 조회를 하면 21 ~ ∞ 까지의 범위가 갭락에 걸리게 되는데 이러한 락은 Next-key Lock이라고 합니다.
이 락은 Next-Key Locks 이라고 합니다.
아래 내용으로 간단히 테스트 해봅시다.
-- 해당 세션의 트랜잭션 격리 레벨을 설정하는 명령어이다.
set transaction_isolation = 'REPEATABLE-READ'
drop table test;
create table test (test integer);
create index test_pk on test(test); -- 인덱스가 있어야만 정상적으로 락이 됩니다.
insert into test values (1);
insert into test values (6);
insert into test values (11);
insert into test values (16);
insert into test values (21);
- 실제 데이터가 있을 때 동등 조회시 발생하는 갭락 테스트
- 실제 데이터가 있을 때 범위 조회시 발생하는 갭락 테스트
- 실제 데이터가 없는 범위에 갭락
배타 락(Exclusive Lock)
배타락은 아래와 같은 쿼리를 실행할 시 발생합니다.
- SELECT ... FOR .. FOR UPDATE
- UPDATE.. SET
- DELETE ... FROM
공유 락을 했을 때의 효과는 다음과 같습니다.
- 배타 락이 걸린 레코드는 DELETE, UPDATE, SELECT FOR UPDATE, SELECT FOR UPDATE 불가능
- 갭 락으로 특정 레코드 범위에 INSERT가 불가능
- 테이블 락으로 테이블 구조 변경 불가능 (컬럼 추가/삭제/수정, 인덱스 추가 등)
레코드 락과 인덱스
- 레코드 락은 인덱스가 있어야만 원하는 레코드에 대해 정확히 락을 걸 수 있습니다.
- 인덱스가 아닌 컬럼은 조건으로 걸어도 레코드 락 대상 선정시 사용되지 않습니다.
- 인덱스가 없을 경우 전체 레코드가 잠금이 됩니다.
레코드 락 대상 선정시 인덱스의 유무가 주는 영향을 테스트 해봅시다.
-- 해당 세션의 트랜잭션 격리 레벨을 설정하는 명령어이다.
set transaction_isolation = 'REPEATABLE-READ'
drop table test;
create table test (indexed_col integer, non_indexed_col integer);
create index test_pk on test(indexed_col ); -- 인덱스가 있어야만 정상적으로 락이 됩니다.
insert into test values (1, 1);
insert into test values (3, 1);
insert into test values (3, 3);
insert into test values (3, 5);
insert into test values (5, 5);
지금까지 트랜잭션과 격리 레벨 그리고 Lock에 대하여 알아봤습니다! 감사합니다!
참고자료
- 위키피디아, 트랜잭션: https://en.wikipedia.org/wiki/Database_transaction
- 위키피디아, ACID: https://en.wikipedia.org/wiki/ACID
- MySQL, Docs: https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-gap-locks
- 2020 유쾌한 스프링 컨퍼런스, 오길환님의 발표
'DB > RDB' 카테고리의 다른 글
[MySQL 8.0] The Physical Structure of an InnoDB Index*** (0) | 2021.10.19 |
---|---|
EXIST VS IN CLAUSE (0) | 2021.10.18 |
시나리오를 통해 Flyway 사용법 학습하기 (1) | 2021.10.05 |
[H2] 원하는 이름으로 된 Non-In-Memory H2 서버 만들기 (0) | 2021.10.04 |
[퍼온 글] MySql 원격 접속 방법 (0) | 2020.06.21 |