트랜잭션이란?
트랜잭션(transaction, xact, txn)이란 데이터베이스에 수행하는 작업의 논리적 단위이다 (연속적으로 들어오는 SQL 명령문들을 사람이 정하는 기준에 따라 나눈 것).
한 트랜잭션에 속한 연산들은 반드시 한꺼번에 모두 수행돼야 할 일련의 작업(“atomic uinit of work”)이다. 계좌이체를 예로 들자면, 한 계좌에서의 출금과 다른 계좌로의 입금이 반드시 동시에 모두 실행돼야 하므로 둘을 묶어 하나의 트랜잭션이라 할 수 있다.
일반적인 트랜잭션의 순서는 다음과 같다:
- 트랜잭션 시작
- 일련의 SQL 연산들…
- 트랜잭션 커밋 혹은 취소
트랜잭션은 성공할 수도, 실패할 수도 있다. 정상적으로 수행됐다면 커밋(commit)하여 종료를 선언한다. 도중에 문제가 생겼다면 취소(abort)하고, 이미 진행한 작업들을 무효화(rollback)한다.
트랜잭션의 성질
트랜잭션은 문제없이 수행되기 위해 ACID 라는 성질을 가져야 한다. 트랜잭션이 ACID하다면, 유저가 정확성에 대해 걱정할 필요가 없어지고, 동시실행이 가능해져 성능이 개선된다.
- 원자성 (Atomicity) : 트랜잭션의 연산은 DB에 모두 반영되거나 아예 반영되지 않아야 한다.
- 일관성 (Consistency) : 트랜잭션 전후로 데이터베이스는 일관성 있는 상태를 유지해야한다.
- 독립성 (Isolation) : 둘 이상의 트렌잭션이 동시에 실행되고 있을 경우 어떤 트랜잭션이라도 다른 트랜잭션의 연산에 끼어들 수 없다.
- 지속성 (Durability) : 성공적으로 수행을 마친 트랜잭션의 결과는 시스템이 고장이 나도 영구적으로 반영되어야한다.
트랜잭션의 격리 수준(isolation)이란?
독립성(Isolation) 관련해, DB는 4개의 격리 수준(isolation level)을 지원한다:
- READ UNCOMMITTED
- Commit 혹은 Rollback 여부에 상관 없이 다른 트랜잭션에서 업데이트된 값을 읽을 수 있다.
- Dirty Read 발생 - 정합성에 문제가 많아 사용하지 않는 것을 권장한다.
- READ COMMITTED
- RDB에서 대부분 기본적으로 사용되는 격리수준이다.
- 실제 테이블 값이 아닌 Undo 영역에 백업된 레코드 값을 가져온다(Logging 참고).
- Dirty Read 현상은 발생하지 않는다.
- 하지만 하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행했을 때는 항상 같은 결과를 가져와야 하는 REPEATABLE READ의 정합성에 어긋난다.
- REPEATABLE READ
- 트랜잭션마다 트랜잭션 ID를 부여해 더 작은 트랜잭션 번호에서 변경한 것만 읽게 한다.
- Undo 공간에 백업해두고 실제 레코드 값을 변경한다. (백업된 데이터는 불필요하다고 판단하는 시점에 주기적으로 삭제한다. Undo에 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있기 때문이다.)
- 이를 MVCC(Multi Version Concurrency Control)라고 부른다.
- Phantom Read 문제가 있다.
- SERIALIZABLE
- 가장 엄격한 격리 수준
- 성능은 가장 떨어진다.
- Phantom Read가 발생하지 않는다. 하지만 데이터베이스에서 거의 사용되지 않는다.
더 자세한 설명은 여기 참고
트랜잭션 매니저의 역할
트랜잭션 매니저(Transaction Manager)는 트랜잭션들의 실행을 관리한다. 트랜잭션 매니저는 여러 레이어(파일, 버퍼, 디스크 등)에 걸쳐 존재한다고 이해할 수 있다.
하나의 데이터베이스를 여러 클라이언트가 조작하는 상황에서, 클라이언트가 생성하는 트랜잭션의 다양한 작업들의 순서를 조율해 데이터베이스의 일관성을 깨트리지 않고 정확한 정보를 효율적이게 전달하는 역할을 한다. 예약 사이트에서 여러 명의 고객이 동시에 다량으로 쿼리문/업데이트문을 생성할 때 중복 예약을 방지하는 처리 등을 한다고 생각하면 된다.
크게 병행 제어(Concurrency Control)와 복구(recovery)를 담당한다. 병행 제어란, 여러 트랜잭션을 병행적으로 수행하며 속도를 개선하고 정확성을 보장하는 작업이다. 복구란, 장애를 허용한다(fault tolerant)는 뜻으로, 소프트웨어적, 시스템적, 미디어적 장애에도 데이터가 오염되지 않게 하는 작업이다. 각각 락킹(two-phase locking)과 로그&회복(write-ahead logging&Recovery)이라는 기법으로 보장한다.
병행 제어 (Concurrency Control)
적절한 병행 제어 없이 트랜잭션들의 동시 실행을 허용할 경우, 다음과 같은 문제점이 발생할 수 있다.
- 갱신 분실 (Lost Update) : 하나의 트랜잭션이 갱신한 내용을 다른 트랜잭션이 덮어써 갱신이 무효화 되는 문제. 두 개 이상의 트랜잭션이 한 개의 데이터를 동시에 갱신할 때 발생한다.
- 모순성 (Inconsistency) : 다른 트랜잭션이 값을 갱신하는 동안, 한 트랜잭션이 어떤 것은 갱신되기 전의 값을 읽고 다른 것은 갱신된 후의 값을 읽어 발생하는 데이터의 불일치 문제.
- 현황파악 오류 (Dirty Read) : 쓰기 작업을 하던 트랜잭션1의 작업 중간 데이터를 다른 트랜잭션2가 읽어 발생하는 문제. 만약 트랜잭션1이 작업을 Rollback한 경우, 트랜잭션2는 무효화된 데이터를 읽어버린 상황이 된다.
- 연쇄 복귀 (Cascading Rollback) : 같은 자원을 사용하는 두개의 트랜잭션 중 하나가 성공적으로 수행했다 하더라도 다른 트랜잭션이 처리하는 과정에서 실패하면 두개가 모두 복구되는 현상.
예시로 이해하기
트랜잭션 P1은 x에 100을 더하려 하고, 트랜잭션 P2는 200을 더하려 한다. 결과는 총 +300 이어야 하지만, P1의 WRITE를 P2의 WRITE가 덮어써 +200 만 반영된 문제가 생겼다.
P2는 10 만큼을 x에서 빼 y에 더하는 작업을 한다. P1은 x와 y의 총합을 구하는데, P2의 두 WRITE 작업 사이에 수행되어 x는 갱신 후의 값을, y는 갱신 전의 값을 읽었기 때문에 “잘못된” 총합이 구해졌다.
트랜잭션 P1은 x에 100을 더하고, 이후 P1의 다른 연산들을 계속 수행한다. 그 사이, P2가 P1이 업데이트한 값을 읽어 X에 100을 뺀다. 그 때, P1의 다른 연산에 문제가 생겨 P1을 Abort + Rollback 해야한다. 즉, P1이 업데이트한 값 +100 이 무효화돼 최종 x의 값은 -100 만 적용돼야한다. 그러나, P2가 업데이트한 값에 따르면 +100 과 -100 이 그대로 적용돼있는 문제가 생겼다.
문제를 해결하기 위한 방법
이러한 문제를 없애는 가장 단순한 방법은 트랜잭션을 하나씩 순차적으로 실행하는 것이다. 이렇게 스케쥴링할 경우, 언제나 안전하지만 당연히 느리다. 그렇다면 트랜잭션을 교차해 실행하면서도 안전을 보장받으려면 어떻게 해야할까?
먼저 몇가지 개념을 알아야 한다.
직렬가능성 (Serializability)
- Serial schedule(직렬 스케쥴) 이란 모든 트랜잭션이 하나씩 순차적으로 실행되는 스케쥴이다. 즉, 각 트랜잭션이 다른 트랜잭션이 끼어드는 일 없이 처음부터 끝까지 한번에 실행된다. 정확성 측면에서 언제나 안전하다.
- Equivalent한 두 스케쥴은:
- 같은 트랜잭션을 포함하며
- 각 트랜잭션의 내부적인 작업(operation) 순서가 동일하고
- 최종적으로 DB를 동일한 상태로 만든다.
-
스케쥴 S가 어떠한 Serial schedule 과 Equivalent 할 때, S를 Serializable schedule(직렬 가능 스케쥴)이라 한다.
➞ 다시 말해, Serializable 트랜잭션 스케쥴이란, 스케쥴에 속한 트랜잭션들이 교차하며 동시에 수행되더라도, 동일한 트랜잭션들이 순차적으로 수행된 Serial 스케쥴과 최종 결과가 동일한 경우를 말한다. Serial 스케쥴의 결과는 정의상 언제나 정확성이 보장되므로, 동일한 결과를 내는 Serializable 스케쥴도 안전할 것이다.
Serializable 스케쥴에는 대표적으로 ‘충돌 직렬(Conflict Serializabililty)’과 ‘뷰 직렬(View Serializability)’이 있다. 충돌직렬이 좀 더 엄격한 직렬 스케줄이기 때문에 모든 충돌 직렬은 뷰 직렬이지만, 그 역은 성립되지 않는다.
충돌 직렬가능성(Conflict Serializability)
앞서 본 직렬가능성 개념의 문제점은 효율이다. 두 트랜잭션이 Equivalent한지 확인하려면 모든 트랜잭션이 완료되어 데이터베이스의 최종 상태가 나올 때까지 기다려야 하기 때문이다. 따라서, Conflict 라는 개념을 추가해 더 효율적인 방법으로 Equivalence를 확인한다.
- 두 작업(operation)이 Conflict한다면, 둘은:
- 서로 다른 트랜잭션에 속하며
- 동일한 레코드에 대해 작업하고
- 적어도 하나는 WRITE 작업이다.
- 두 스케쥴이 Conflict equivalent하다면, 둘은:
- 동일한 트랜잭션을 포함하며
- 모든 conflicting 작업의 쌍은 동일한 순서로 실행된다.
- 스케쥴 S가 어떠한 Serial schedule과 Conflict equivalent 할 때, S를 Conflict serializable schedule(충돌 직렬 가능 스케쥴)이라 한다.
➞ 이 Conflict serializability가 대부분의 상용 데이터베이스의 병행 제어에서 정확성/안전함을 판단하는 기준이다. 즉, 모든 병행 제어 기법은 Conflict serializability를 추구한다.
(참고로, 모든 serializable 스케쥴이 conflict serializable한 것은 아니다. 따라서 안전한 serializable 스케쥴을 위험하다 판단해 abort 하는 경우도 존재한다. 하지만 실용성 때문에 사용중.)
충돌 직렬가능성을 확인하는 방법
-
방법 1. 순서를 바꿔 Serial 스케쥴이 만들어지는지 확인한다.
트랜잭션들의 작업들 중, (1) 연속되게 위치하고 (2) Conflicting 하지 않은 작업들의 순서를 서로 바꿈으로써 Serial 스케쥴이 만들어진다면, 해당 스케쥴은 Conflict serializable하다.
-
방법 2. Dependency graph를 확인한다.
Dependency Graph는 다음 규칙으로 만들어진다:
- 트랜잭션마다 노드를 생성한다.
- 다음 조건을 만족할 때 노드 Ti ➞ 노드 Tj 로 edge를 그린다:
- Ti의 작업 x가 Tj의 작업 y와 conflict 하면서
- x가 y에 (스케쥴상) 선행한다.
만들어진 그래프에 사이클이 없다면(acyclic), Conflict serializable 스케쥴이다.
➞ 단, 이는 개념적 설명이지 실질적으로 사용되는 방법은 아니다!!
뷰 직렬가능성(View Serializability)
또다른 Serializable 개념으로는 뷰 직렬가능성이 있다.
- 스케쥴 S와 S’가 다음 조건을 만족한다면, 둘은 View equivalent하다:
- Same initial reads : S에서 트랜잭션 T1이 데이터 Q의 initial value를 읽는다면, S’에서도 T1은 Q의 initial value를 읽는다.
- Same dependent reads : S에서 T1이 업데이트한 Q 값을 T2가 읽는다면, S’에서도 T1이 업데이트한 Q 값을 T2가 읽는다.
- Same winning writes : S에서 T1이 마지막 write(Q)를 수행한다면, S’에서도 T1이 마지막 write(Q)를 한다.
- 어떠한 Serial 스케쥴과 View equivalent하다면, 이는 View serializable schedule(뷰 직렬 가능 스케쥴)이다.
말이 조금 어려운데, 핵심은 Serial 스케쥴 S과 비교해서 스케쥴 S’가 View serializable하려면, 동일한 데이터 Q에 대한 ‘초기값 읽기’, ‘다른 트랜잭션이 업데이트한 값 읽기’, ‘마지막 쓰기’에서 트랜잭션들의 실행 순서만 동일하다면 된다는 것이다. 기존 Conflict serializable 스케쥴에 더해 “blind writes”를 허용한다.
(참고로, 뷰 직렬 또한 모든 serializable 스케쥴을 허용하진 않는다.)
왜 충돌 직렬가능성을 사용하나요?
Serializability 나 view serializability 개념 대신 Conflict serializability 를 사용하는 이유는 효율이다. 실제 상황에선 새로운 트랜잭션이 끊임없이 입력되기 때문에 스케쥴의 경계를 정하는 것 자체가 어려울 뿐 아니라, 임의로 경계를 정했다 하더라도 모든 스케쥴을 확인하는 데 아주 많은 시간이 걸린다.