동시성 제어 Concurrency Control
는 다수의 트랜잭션이 동시에 실행될 때 데이터 일관성, 무결성을 보장하기 위한 DBMS의 기능입니다.
다수의 트랜잭션이 하나의 데이터에 접근하게되는 상황을 정리하면 다음 세 가지 경우가 존재합니다.
트랜잭션 x | 트랜잭션 y | 문제 상황 | 동시 접근 |
---|---|---|---|
읽기 | 읽기 | 읽기는 문제 발생 X | 가능 |
읽기 | 쓰기 | Dirty read, 반복불가능 읽기, 유령 데이터 읽기 | 가능 or 불가능 선택 |
쓰기 | 쓰기 | 갱신 손실 문제 | 불가능 |
첫 번째 상황인 [읽기, 읽기] 동시 접근에서는 아무런 문제가 발생하지 않습니다. 두 번째와 세 번째 상황인 [읽기, 쓰기], [쓰기, 쓰기] 동시 접근 상황에서는 표에 적힌 문제들이 발생하게 됩니다.
지금부터 문제 상황 두 가지에 대한 해결 방법에 대해서 알아보겠습니다.
트랜잭션 x | 트랜잭션 y | 문제 상황 | 동시 접근 |
---|---|---|---|
읽기 | 쓰기 | Dirty read, 반복불가능 읽기, 유령 데이터 읽기 | 가능 or 불가능 선택 |
하나의 데이터에 두 개의 트랜잭션이 [읽기, 쓰기] 동시 접근을 하는 경우 읽기를 수행하는 트랜잭션에서 Dirty read, 반복불가능 읽기, 유령 데이터 읽기
와 같은 문제를 발생시킵니다.
문제가 발생하는 원인은 쓰기 트랜잭션에 의해 조작되는 데이터를 중간에 읽게 되기 때문입니다.
Dirty read (또는 uncommitted dependency)
는 읽기 트랜잭션이 쓰기 트랜잭션이 작업하고 있는 데이터를 중간에 읽을 때 발생하는 문제입니다. 쓰기 트랜잭션에서 문제가 발생하여 이를 ROLLBACK
하는 경우 읽기 트랜잭션은 작업 취소한 데이터를 읽어버리게 됩니다.
예시 SQL은 간결하게 추상화하여 구문을 작성했습니다.
a 테이블에 data = 10
이 저장되어 있다고 가정합니다.
#읽기 트랜잭션
SELECT data FROM a #결과: 10
#쓰기 트랜잭션
UPDATE a SET data = 0;
#읽기 트랜잭션
SELECT data FROM a #결과: 0
#쓰기 트랜잭션
ROLLBACK; #이전에 읽기 트랜잭션이 읽은 값은 무효된 데이터
반복 데이터 읽기
는 읽기 트랜잭션이 데이터를 읽고 쓰기 트랜잭션에 의해 데이터가 새로 쓰여진 뒤 다시 읽기 트랜잭션이 실행되면 동일한 SQL에 대해 이전과는 다른 값(새로 갱신된 값)이 도출되는 문제입니다.
#읽기 트랜잭션
SELECT data FROM a #결과: 10
#쓰기 트랜잭션
UPDATE a SET data = 0;
COMMIT;
#읽기 트랜잭션
SELECT data FROM a #결과: 0, 동일한 읽기 트랜잭션 SQL이지만 이전 수행과 결과가 다름
유령 데이터 읽기
는 읽기 트랜잭션이 읽기 작업 후에 쓰기 트랜잭션이 INSERT를 하는 경우, 읽기 트랜잭션이 다시 실행되면 첫 번째 실행에 없던 새로운 데이터(유령 데이터)가 나타나는 문제입니다.
#읽기 트랜잭션
SELECT data FROM a #결과: 10
#쓰기 트랜잭션
INSERT INTO a VALUES (1);
COMMIT;
#읽기 트랜잭션
SELECT data FROM a #결과: 10, 1, 동일한 읽기 트랜잭션 SQL이지만 이전 수행과 결과가 다름
MySQL
에서는 유령 데이터 읽기 현상이 발생하지 않습니다.
InnoDB
엔진에서REPEATABLE READ
가 트랜잭션이 처음 데이터를 읽는 순간의 스냅샷만을 보고 트랜잭션을 수행하기 때문에 실행결과가 동일하게10
만 나타나게 됩니다.
위와 같은 문제들을 해결하기 위해서 DBMS는 네 가지 명령어들을 제공하고 있으며 이 명령어를 트랜잭션 고립 수준 명령어 Transaction Isolation Level Instruction
이라고 부릅니다. 이 명령어들을 사용하여 프로그래머가 트랜잭션을 제어할 수 있습니다.
SQL 표준에서 정의하는 네 가지 고립 수준은 다음과 같습니다. (DBMS 마다 약간 차이 있을 수 있음)
고립 수준/문제 | Dirty read | 반복불가능 읽기 | 유령 데이터 읽기 |
---|---|---|---|
READ UNCOMMITTED | O | O | O |
READ COMMITED | X | O | O |
REPEATABLE READ | X | X | O |
SERIALIZABLE | X | X | X |
READ UNCOMMITTED
는 가장 낮은 레벨의 고립 수준 명령어입니다.
데이터에 아무런 공유락을 걸지 않고 배타락만 걸어줍니다. 그리고 다른 트랜잭션에 공유락/배타락이 걸린 데이터를 대기 없이 읽습니다. COMMIT
되지 않은 데이터까지 읽습니다.
공유락
,배타락
은 잠시 후에 소개드릴 예정이나 간략히 설명하면 다음과 같습니다.
공유락
: Shared Lock, 읽기에 사용되는 락배타락
: Exclusive Lock, 읽기/쓰기에 사용되는 락
READ COMMITTED
은 Dirty read를 피하기 위해 데이터를 읽는 동안 공유락을 걸어 놓는 명령어입니다. 설정된 공유락은 트랜잭션 도중에 해제할 수 있으며, 마찬가지로 배타락은 항상 걸어둡니다.
REPEATABLE READ
은 데이터에 설정된 공유락과 배타락을 트랜잭션 종료시까지 유지하여 다른 트랜잭션이 쓰기 동작을 하지 못하게 만듭니다.
SERIALIZABLE
은 가장 고립 수준이 높은 명령어로 지금 실행 중인 트랜잭션이 다른 트랜잭션으로부터 완전히 분리됩니다.
하나의 데이터에 대해 [쓰기, 쓰기] 동시 접근 상황에서는 갱신 손실 Lost Update
라는 문제가 발생합니다. 상황 2의 문제는 읽기 상황에서 발생하는 문제들이라 치명적이지 않지만 갱신 손실
은 쓰기에 문제가 발생하므로 절대로 발생하면 안되는 문제입니다.
초기 잔액(balance)가 100으로 설정되었다는 가정 하에 출금 A, 입금 B 트랜잭션을 통해 갱신 손실
문제에 대해서 알아보겠습니다.
100원 계좌에서 10원을 출금한 뒤 20원을 입금하도록 트랜잭션을 짠다면 기대하는 값은 잔액이 110원이어야 하겠죠?
# A 시작
START TRANSACTION;
SELECT balance
FROM accounts
WHERE account_id = 1;
# 결과: 100
# A2: 출금 10원 계산, (100 - 10 = 90)
UPDATE accounts
SET balance = 90
WHERE account_id = 1;
# 아직 커밋하지 않음
# B 시작 (A가 아직 커밋 전)
START TRANSACTION;
SELECT balance
FROM accounts
WHERE account_id = 1;
# 결과: 100 ← A의 변경분(90)을 보지 못하고 여전히 100
# B2: 입금 20원 계산, (100 + 20 = 120)
UPDATE accounts
SET balance = 120
WHERE account_id = 1;
#A 작업 커밋 후 B 커밋
입금 B 트랜잭션 동작이 출금 A 트랜잭션의 변경 결과를 무시(커밋하지 않아서)하고 작업을 하기 때문에 최종 결과가 120원이 되어 출금을 수행했던 기록이 사라지게 됩니다.
UPDATE 작업이 사라진 이런 문제를 갱신 손실
이라고 부르며 갱신 손실
을 막기 위해서 락 Lock
이라는 기능을 사용합니다.
락 Lock
은 현재 트랜잭션이 다른 트랜잭션에 데이터를 사용(읽기/쓰기)하고 있다고 알리는 기능입니다. 한 트랜잭션이 락
을 사용해 데이터를 잠구면 다른 트랙잭션은 락이 풀릴 때까지 기다렸다가 데이터에 접근해야 합니다.
락
을 걸게되면 다른 트랜잭션이 대기하게 되면 사용자도 마찬가지로 응답을 받는데 시간이 소모되기 때문에 불편함을 줄 수 있습니다.
데이터에는 읽기만 하거나 쓰기만 하거나 읽고 쓰기를 하는 데이터가 있는데 읽기만 하는 데이터는 다른 트랙잭션의 접근을 허용해도 큰 문제가 없습니다. 그렇기 때문에 락
도 읽기와 쓰기 두 가지 용도로 나누어서 사용됩니다.
공유락 LS, shared lock
은 트랜잭션이 읽기를 할 때 사용하는 락이고 배타락 LX, exclusive lock
은 읽기/쓰기를 할 때 사용하는 락입니다.
트랜잭션이 데이터에 락을 거는 규칙은 다음과 같습니다.
LS
, 읽기 또는 쓰기를 하는 경우 LX
를 요청한다.LS
를 걸었다면 LS
요청은 허용하고 LX
요청은 불허한다.LX
를 걸었다면 LS, LX
를 모두 불허한다.락
을 걸거나 해제하는 시점에 따로 제한을 두지 않으면 두 트랜잭션이 동시 실행되는 과정에서 데이터 일관성이 깨질 수 있습니다. 해제 시점에 다른 트랜잭션이 트랜잭션의 중간 실행 결과를 읽어버릴 수 있다는 점을 방지하기 위해 2단계 락킹
기법을 사용합니다.
2단계 락킹
기법은 락을 걸고 해제하는 시점을 다음과 같이 2단계로 나누어서 진행합니다.
두 개의 트랜잭션이 자신의 데이터에 락을 걸고, 상대방 데이터에 락을 요청하면 서로가 잠군 데이터를 요청하게 되므로 무한 대기 상태가 발생합니다. 이러한 현상을 교착상태 또는 데드락
이라고 부릅니다.
DBMS에서는 교착상태를 해결하기 위해 교착상태가 발생하면 두 작업 중 하나를 강제 중단시킵니다. 그리고 강제 중단된 트랜잭션에서 변경된 데이터는 처음의 상태로 되돌려놓습니다. 이렇게 락을 해제함으로써 교착상태를 해결합니다.