트랜잭션과 잠금은 비슷해보이지만 목적에서 그 차이를 보입니다.
트랜잭션은 All or Nothing을 통해 데이터의 일부분만 업데이트될 수 있는 상황에 대비해 커밋이나 롤백등을 통해 데이터의 정합성이 깨지는것을 막아줍니다.
**잠금(Lock)**은 스레드간 동시성을 보장하기 위한 방법입니다. 레코드,테이블,파일 등 데이터에 잠금을 획득한 스레드는 데이터를 읽거나 수정할 수 있으며, 다른 스레드를 블로킹해 간섭을 통한 데이터 조작 시 이상현상이 발생하지 않도록 합니다. 이를 통해 트랜잭션의 처리 순서가 지정됩니다.
트랜잭션 격리수준은 여러 트랜잭션이 작업 내용을 어느 수준으로 공유하는지에 대한 내용입니다.
[본 문서는 책 Real MySQL과 MySQL공식문서를 참고해 작성되었으므로, 별다른 표기가 없다면 InnoDB의 동작을 기준으로 설명합니다.]
트랜잭션이란 ACID라는 성질을 만족하는 데이터 처리 기법입니다.
Atomicity: 원자성, 데이터의 처리가 전부 진행되거나 하나도 진행되지 않는 것입니다. 쿼리의 일부만 반영되는것을 방지합니다. 이는 BEGIN,COMMIT,ROLLBACK을 통해 보장합니다.
Consistency: 일관성, 데이터 처리를 통해 무결성이 위배되지 않아야 한다는 특성입니다. 트랜잭션 후 무결성 제약조건이 위배되는 등 일관성을 갖지 못한 트랜잭션은 중단됩니다.
Isolation: 독립성, 다른 트랜잭션의 실행과 상관없이 동작하며, 트랜잭션 중간에 데이터가 어떻게 변경되는지 다른 트랜잭션이 몰라야 합니다. (이는 성능과 관련된 이슈로 인해 Isolation Level로 나뉘며, 유연하게 적용됩니다. Lock을 통해 보장할 수 있습니다.)
Durability: 지속성, 완료된 트랜잭션은 영구적으로 반영되어야함을 의미합니다. 시스템 장애로 인해 데이터가 손실될 경우 로그를 통해 복구할 수 있어야 합니다. InnoDB기준 리두로그를 통해 보장할 수 있습니다.
InnoDB와 달리 MyISAM, MEMORY 스토리지 엔진에서는 트랜잭션을 제공하지 않습니다. 그러므로 인해 더욱 빠른 속도의 쿼리 처리가 가능하지만, 트랜잭션의 성질을 보장받지 못합니다.
일례로, 다음과 같은 쿼리를 작성해보면 MyISAM과 innoDB의 트랜잭션 존재여부 차이를 확인할 수 있습니다.
# InnoDB
CREATE TABLE TEST( K INT, PRIMARY KEY(COL)) ENGINE=INNODB;
INSERT INTO TEST(K) VALUES (3);
# 1)데이터 1개 삽입
INSERT INTO TEST(K) VALUES (1),(2),(3);
사진에서 확인할 수 있듯, 값 3개를 넣는 INSERT문은 PK의 중복 불가 제약조건에 의해 일관성을 위배하게 되어 트랜잭션이 종료됩니다.
또한 SELECT쿼리로 조회했을 때는 트랜잭션의 원자성을 보장하므로 1,2 값이 적용되지 않은것을 확인할 수 있습니다.
MyISAM엔진으로 테이블을 생성해 쿼리를 수행하면, INSERT쿼리에서 1,2번은 제약조건에 걸리지 않아 1,2,3이 모두 삽입되는것을 확인할 수 있습니다.
트랜잭션은 Isolation이라는 특성이 존재합니다. Isolation(독립성)이란, 실행중인 트랙잭션 외 다른 트랜잭션이 자신 트랜잭션이 수정하는 정보를 읽거나 수정하지 못하게 해 무결성을 보장해주는 특성입니다.
그러나 동시에 많은 요청이 발생하는 데이터베이스 서버 특성 상 트랜잭션의 완전한 격리는 필연적으로 성능의 저하를 불러일으킵니다. 그렇기에 트랜잭션의 **동시성(Concurrency)과 독립성(Isolation)**은 성능과 정합성의 등가 교환관계에 있습니다.
따라서, 데이터베이스 시스템에서는 트랜잭션의 격리수준을 4가지 단계로 나눠 시스템 관리자가 쿼리의 특성에 따라 격리수준을 세팅할 수 있도록 했습니다.
독립성 보장(정합성) | < === === | === === > | 동시성 보장(성능) |
---|---|---|---|
Serializable | Repeatable Read | Read Committed | Read Uncommitted |
트랜잭션은 BEGIN, COMMIT, ROLLBACK을 통해 작업의 원자성을 보장합니다.
아래 그림들에서도 BEGIN, COMMIT을 기준으로 트랜잭션의 끝과 시작을 표기했습니다.
독립성을 보장할수록 성능은 떨어지지만 동시성을 가져갈수록 성능은 향상되지만 데이터 정합성과 관련된 많은 문제들이 발생합니다.
동시성이 더 높은 격리수준에서 발생하는 정합성 문제는 더 낮은 수준의 동시성의 격리수준에서도 발생하므로, 격리수준이 낮은 설정부터 살펴보겠습니다.
innoDB는 블로킹 없이도 Repeatable Read격리수준을 보장합니다.
Read Uncommitted, Read Committed는 innoDB 스토리지 엔진을 사용하는 경우 사용되지 않습니다. Undo Log를 통해 Repeatable Read격리수준까지 블로킹 없는 일관된 읽기를 지원하기 때문인데요, [이전 글 링크]를 참조해 자세한 설명을 확인하거나, Repeatable Read에서의 설명을 확인하면 됩니다.
Read Uncommitted 격리수준은 격리성을 보장하지 않고 동시성을 가져가기 위한 격리수준으로, 사실상 트랜잭션 격리를 하지 않는것과 같습니다.
그렇기에 데이터를 수정하기 위해 X락을 가져가는 트랜잭션 A(왼쪽)가 변경한 데이터가 커밋되지 않았더라도 변경한 데이터를 조회할 수 있습니다.
Read Uncommitted에서는 Dirty Read문제가 발생합니다.
Dirty Read는 A 트랜잭션이 변경한 데이터를 B 트랜잭션이 읽었지만, A 트랜잭션이 ROLLBACK되어 B트랜잭션이 오염된 데이터를 읽는 경우의 예시를 들 수 있습니다.
[해당 격리수준에서는 Dirty Read 문제가 해결됨을 확인할 수 있습니다.]
Read Committed격리수준에서는 Commit된 데이터만 읽어 앞선 Read Uncommitted의 Dirty Read문제를 해결합니다.
A 트랜잭션이 수정되었지만 커밋되지 않은 경우, 수정 이전의 커밋된 데이터를 읽습니다. 그렇기에 A 트랜잭션이 롤백되어도 아무런 문제가 없죠!
Read Committed 격리수준에서는 Non Repeatable Read문제가 발생합니다.
Non Repeatable Read란, B트랜잭션(오른쪽)이 데이터를 읽을 때, 동일한 트랜잭션에서 데이터를 두 번 읽었지만 A트랜잭션 커밋 여부에 따라 다른 결과값을 가져오는 문제입니다.
위 그림에서 B 트랜잭션은 동일한 SELECT쿼리를 통해 같은 데이터를 조회했음에도, 다른 결과값을 가져옵니다.
예를들어 아래와 같은 쿼리에서 문제를 발생시킬 수 있습니다.
# Transaction B's Query
SELECT * FROM
(SELECT * FROM MEMBER WHERE NAME='marco')
union # <- 이 시점에 A쿼리의 데이터 marco -> mark로 수정 및 커밋
(SELECT * FROM MEMBER WHERE NAME='mark')
B 트랜잭션에서는 NAME=’mark’인 레코드를 찾습니다. 이후 NAME=’marco’인 레코드들을 찾기 전에 A트랜잭션이 mark를 marco로 변경한다면, union의 위,아래 결과 같은 레코드가 포함되어 정합성을 해치는 문제점이 발생합니다.
(물론 쿼리 조건에서 NAME = ‘mark’ or NAME=’marco’로 구성했다면 문제가 발생하지 않지만, 그럴듯한 예시가 떠오르지 않아 약간 작위적으로 구성했습니다. )
Repeatable Read 격리수준에서는 Non Repeatable Read문제를 해결합니다.
레코드 컬럼중 transaction id를 나타내는 컬럼을 확인할 수 있습니다. transaction id는 트랜잭션이 시작한 순서대로 커지는 값입니다. 트랜잭션의 시작 순서를 나타낸다고 봐도 무방합니다.
트랜잭션B를 기준으로 레코드의 트랜잭션 id가 낮은 레코드만 읽는다면, 트랜잭션 B 이후에 시작된 트랜잭션이 데이터를 변경 및 커밋해 Non Repeatable Read가 발생하지 않을 확률이 높습니다.
그러나 트랜잭션B보다 먼저 시작했지만 트랜잭션B가 읽기작업을 하는 시점에도 커밋되지 않은 트랜잭션(A라고 합시다)이 존재할 수 있습니다. 이 경우에는 실행시점이 늦은 트랜잭션B가 블로킹되어, 트랜잭션A가 커밋될때까지 기다립니다.
앞에서 언급했듯, InnoDB는 기본적으로 Blocking없는 Repeatable Read 격리수준을 지원합니다.
바로 Undo Log라는 추가적인 공간을 사용하기때문에 가능한데요, InnoDB는 트랜잭션이 데이터를 변경할 때, 이전 데이터를 Undo Log에 복사하고, B트랜잭션은 id가 더 낮은 Undo Log의 레코드를 읽어 데이터 일관성을 유지합니다.
Q. [Repeatable Read에서..]
Phantom Read와 gap lock에 대해 좀더 공부하기
MySQL 공식문서, innoDB 트랜잭션 격리수준
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html