트랜잭션 : 애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법. → 하나의 트랜잭션 내부의 모든 읽기와 쓰기는 하나의 연산. 따라서 전체 성공(커밋) or 전체 실패(롤백, abort)
트랜잭션이 항상 필요한 것은 아님. 오히려 트랜잭션을 쓰지 않을 때 성능 향상, 가용성 등의 이득을 얻을 수도 있다. 따라서 언제 써야 할지 판단할 줄 알아야 함.
학습 목표: 트랜잭션으로 문제가 생길 수 있는 사례들을 조사하고 이를 방지하기 위한 DB의 알고리즘을 살펴본다. 동시성 제어 분야와 DB에서 커밋 후 읽기, 스냅숏 격리, 직렬성 같은 격리 수준을 구현하는 방법을 알아본다.
1. 애매모호한 트랜잭션의 개념
트랜잭션을 둘러싼 두 가지 주장 :
트랜잭션은 확장성의 안티패턴이며 높은 성능과 고가용성을 유지하기 위해 트랜잭션을 포기해야 한다.
트랜잭션 보장은 값진 데이터가 있는 중대한 애플리케이션에 필수적인 요구사항
→ 둘 다 과장이다. 정상적인 운영상황과 극단적인 운영 상황에서 트랜잭션이 보장하는 내용을 통해 트랜잭션의 트레이드 오프에 대한 이해가 필요.
ACID의 의미
DB마다 ACID 구현이 다 다름.
Atomicity (원자성): 결함 때문에 완료 될 수 없다면 롤백되고 DB는 이 트랜잭션에서 지금가지 실행한 쓰기를 무시하거나 취소한다.
Consistency (일관성): 데이터는 항상 진실이어야 하는 선언(불변식)이 존재한다. 불변식을 위반하는 잘못된 데이터를 쓰지 않도록 막아야 한다. (ACID 중 유일하게 DB가 아닌 애플리케이션의 속성.)
Isolation (격리): 동시에 실행되는 트랜잭션은 서로 격리되어 다른 트랜잭션을 방해할 수 없다. 동시에 실행되는 트랜잭션 이후 결과는 순차적으로 실행되는 트랜잭션 이후 결과와 동일해야 한다. 직렬성 = 각 트랜잭션이 전체 DB에서 실행되는 유일한 트랜잭션인 것처럼 동작함. → 성능 손해로 현실에선 거의 사용하지 않음 → 직렬성 보다 보장이 약한 스냅숏 격리 구현
Durability (지속성): 트랜잭션이 성공적으로 커밋되면 어떤 결함이 발생해도 기록한 모든 데이터는 손실되지 않는다는 보장
복제와 지속성 : 지속성을 위해 최근에는 하드디스크에 기록을 넘어서 복제를 사용하기도 함. 지속성 보장을 위해 여러 가지 방법이 있지만 절대적 보장은 없다.
단일 객체 연산과 다중 객체 연산
원자성 & 격리성 : 한 트랜잭션 내에서 여러 번의 쓰기를 하면 DB가 어떻게 해야 하는지를 서술.
다중 객체 트랜잭션: 한 트랜잭션에서 여러 객체(로우, 문서, 레코드)를 변경할 수 있다. 데이터의 여러 조각이 동기화된 상태로 유지되어야 할 때 필요
RDB에서 트랜잭션 적용 방법 : 클라이언트와 데이터베이스 서버 사이의 TCP 연결을 기반으로, 특정 연결 내의 BEGIN TRANSACTION ~ COMMIT 문 사이를 하나의 트랜잭션으로 간주
NoSQL에서 트랜잭션 적용 방법 : 연산을 묶는 방법이 없는 경우가 많다.
단일 객체 쓰기
단일 객체 쓰기에서도 원자성과 격리성이 적용된다.
한 노드에 존재하는 단일 객체 수준에서 원자성과 격리성을 제공.
원자성 - 장애 복구용 로그를 써서 구현 (≈B트리)
격리성 - 각 객체에 잠금을 사용(동시에 한 스레드만 객체에 접근하도록)
단일 객체 연산은 여러 클라이언트의 동시 한 객체 쓰기 시도에서 갱신 손실을 방지한다. 하지만 일반적인 의미의 트랜잭션 X
트랜잭션 : 다중 객체에 대한 다중 연산을 하나의 실행 단위로 묶는 매커니즘
다중 객체 트랜잭션의 필요성
필요성 사례
관계형 데이터 모델 : 외래 키를 갖는 경우 (혹은 그래프에서 다른 정점에 연결된 간선이 있는 경우) 참조가 유효한 상태로 유지되도록 보장해야 한다.
문서 데이터 모델 : 조인 기능이 없으면 비정규화를 장려. 비정규화된 정보 갱신 시 한 번에 여러 문서를 갱신해야 한다. 이 때 트랜잭션으로 비정규화된 데이터가 동기화 깨지는 것을 방지할 수 있다.
보조 색인이 있는 데이터베이스 : 값 변경 시 색인도 변경되어야 함.
트랜잭션으로 원자성과 격리성을 보장하면 쉽게 위의 내용들을 해결할 수 있음.
오류와 어보트 처리
어보트 처리를 하지 않는 경우 : 리더 없는 복제의 경우. 오류가 발생하면 이미 한 일은 취소하지 않기 때문에 오류 복구는 애플리케이션의 책임
어보트 처리의 목적 : 안전하게 재시도하기 위함. 따라서 어보트된 후에 재시도를 하지 않고 사용자의 입력이 사라지는 것은 안타까운 일이다.
어보트된 트랜잭션 재시도 : 효과적인 오류 처리 매커니즘이지만 완벽하지는 않다.
트랜잭션은 성공했으나 커밋 성공을 알리는 도중 실패할 경우, 재시도하면 트랜잭션은 두 번 커밋됨. ∴ 애플리케이션에 추가적인 중복 제거 매커니즘
과부하로 인한 오류라면 트랜잭션 재시도로 악화될 수 있다. ∴ 재시도 횟수 제한, 지수적 백오프, or 과부화와 관련된 오류는 별도 처리
일시적인 오류만 재시도 가치가 있고, 영구적인 오류는 재시도 가치가 없음
트랜잭션이 부수 효과가 있다면 어보트 시에도 부수 효과가 실행될 수 있다. ex) 이메일을 보내는 경우, 재시도에도 이메일을 보내고 싶지는 않음
위 사례에서 앨리스는 은행 계좌를 새로고침하면 일관성 있는 계좌 잔고를 볼 수 있겠지만, 이런 일시적인 비일관성을 참을 수 없는 경우도 있다.
일시적인 비일관성이 문제가 되는 경우
백업 : 백업 시 사용되는 DB 전체 복사본은 크기가 크다. 따라서 저장되는 데 시간이 오래 걸릴 수 있고 그 시간동안에도 데이터 쓰기가 실행되므로 백업 데이터의 일부는 오래된 데이터, 일부는 최신화된 데이터일 수 있다.
분석 질의와 무결성 확인
스냅숏 격리: 각 트랜잭션은 DB의 일관된 스냅샷으로부터 읽는다. = 트랜잭션은 특정한 시점의 과거 데이터를 보는 것. 백업이나 분석처럼 실행하는 데 오래 걸리고 읽기만 실행하는 질의에서 유용
스냅숏 격리 구현
핵심 원리 : 읽는 쪽에서 쓰는 쪽을 결코 차단하지 않고 쓰는 쪽에서 읽는 쪽을 결코 차단하지 않는다. (더티 쓰기를 방지하기 위해 쓰기 잠금을 사용하지만 읽을 때는 아무 잠금도 사용하지 않음)
DB는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 한다. = 다중 버전 동시성 제어 (multi-version concurrency control, MVCC)
커밋 후 읽기 격리 : 객체마다 버전은 최대 두 개씩. (커밋된 버전, 덮어 쓰여졌지만 아직 커밋되지 않은 버전). 질의마다 독립된 스냅숏을 사용
스냅숏 격리 : 커밋 후 읽기 격리를 위해서도 MVCC를 사용. 전체 트랜잭션에 대해 동일한 스냅숏을 사용p. 239
MVCC 기반 스냅숏 격리 구현 방법 :
트랜잭션 시작 시 고유한 트랜잭션 ID (txid)를 할당받음. (계속 증가)
데이터베이스에 쓰기를 진행할 때마다 트랜잭션의 ID가 함께 붙는다.
테이블의 각 로우에는 created_by (생성한 트랜잭션 ID를 저장하는 필드), deleted_by (처음에는 비어있는 필드) 가 존재한다.
트랜잭션이 로우 삭제 시 deleted_by에 트랜잭션 ID를 적어두고 완전히 접근하지 않는게 확인되면 GC가 데이터를 삭제한다.
갱신은 내부에서삭제와생성으로 변환된다. 트랜잭션13이 400 → 500으로 수정한다면, 로우가 두 개 존재하여 400은 삭제된 로우(deleted_by = 트랜잭션 13), 500은 생성된 로우(created_by = 트랜잭션 13)로 보인다.
일관된 스냅숏을 보는 가시성 규칙
트랜잭션 ID를 이용한 가시성 규칙
각 트랜잭션을 시작할 때 그 시점에 진행중인 모든 트랜잭션의 목록을 리스트업. 이 목록의 트랜잭션들이 쓴 데이터는 전부 무시. (목록 내 트랜잭션이 커밋되더라도)
어보트된 트랜잭션이 쓴 데이터는 무시
트랜잭션 ID가 더 큰 (= 나중에 쓰인) 데이터는 커밋 여부 무관하게 무시
그 밖의 모든 데이터는 애플리케이션의 질의로 볼 수 있다.
색인과 스냅숏 격리
다중 버전 DB에서의 색인 동작 방법
색인이 객체의 모든 버전을 가리키게 하고 색인 질의에서 현재 트랜잭션에서 볼 수 없는 버전을 걸러내게 함. GC가 오래된 객체 버전을 삭제할 때 대응되는 색인도 삭제
동일한 객체의 다른 버전들이 같은 페이지에 저장될 수 있다면 색인 갱신을 회피하여 최적화
추가 전용 (append-only) + 쓸 때 복사되는 (copy-on-write) 변종 B 트리를 이용해 트리의 페이지가 갱신될 때 덮어쓰는 대신 각 변경된 페이지의 새로운 복사본을 생성한다. 부모 페이지들은 복사되고, 자식 페이지들의 새 버전을 가리키도록 갱신된다. 새 B 트리의 루트는 DB 스냅숏의 일부가 된다.
댓글