Search
📖

[데이터 중심 애플리케이션 설계] 7. 트랜잭션

내용 정리

0. 서론

트랜잭션
애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법
격리 수준
커밋 후 읽기 (read committed)
스냅숏 격리 (repeatable read)
직렬성 격리 (serializable)

1. 트랜잭션과 ACID

트랜잭션
애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법
안정성 보장
애플리케이션에서 발생 가능한 잠재적인 오류 시나리오와 동시성 문제를 데이베이스 레벨에서 해결
안정성 보장의 종류 (ACID)
원자성(Atomic)
일관성 (Consistency)
격리성 (Isolation)
지속성 (Durability)
cf) BASE
ACID 표준을 따르지 않는 시스템
Basically Available (기본적으로 가용성을 제공)
Soft state (유연한 상태)
Eventual consistency (최종적 일관성)

1.1. 원자성 (Atomicity)

정의
더 작은 부분으로 쪼갤 수 없는 뭔가
전부 반영하거나 전부 반영하지 않거나. 부분 실패 X
특징
원자적 연산
실행 하기 전 또는 실행한 후 상태만 존재
오류 발생 시 트랜잭션 어보트, 기록된 모든 내용을 취소 하는 능력
어보트 능력 (Abortability)

1.2. 일관성 (Consistency)

정의
항상 진실이어야 하는, 데이터에 관한 어떤 선언(불변식)이 존재한다는 것
특징
애플리케이션의 불변식 개념에 의존
일관성 유지를 위해 트랜잭션을 올바르게 정의하는 것은 애플리케이션의 책임
예)
회계 시스템 차변/대변 일치

1.3. 격리성 (Isolation)

정의
동시에 실행되는 트랜잭션이 서로 격리됨을 의미
특징
동시 실생되는 트랜잭션은 서로 방해 X
1개의 트랜잭션의 여러번 쓰기 → 다른 트랜잭션은 내용을 전부 보거나, 아무것도 보지 못하거나
직렬성
A 트랜잭션은 B 트랜잭션을 방해할 수 없음
순차적으로 실행

1.4. 지속성 (Durability)

정의
결함이 발생하더라도 트랜잭션에서 기록한 데이터가 손실되지 않는 다는 것을 보장
예)
WAL (write ahead log)
복제 기능의 노드

1.5. 단일 객체 연산과 다중 객체 연산

다중 객체 트랜잭션
한번의 트랜잭션에 여러 객체(로우, 문서, 레코드)를 변경할 수 있음
여러 객체가 동기화된 상태를 유지해야함
어떤 읽기 연산과 어떤 쓰기연산이 동일한 트랜잭션에 속하는지 알아낼 수단 필요
클라이언트와 데이터베이스 서버 사이의 TCP 연결 기반
예)
BEGIN TRANSACTION & COMMIT
단일 객체 트랜잭션
단일 객체 수준에서 원자성과 격리성 제공 목표
원자성 - 장애 복구용 로그 (crash recovery)
격리성 - 각 객체에 잠금 (동시에 한 스레드만 접근 가능)
원자적 연산 기능 제공
read-modify-write 주기 제거
compare-and-set 연산
변경하려는 값이 누군가에 의해 동시에 바뀌지 않았을 때만 쓰기 반영
동시 객체 쓰기 시 갱신 손실 방지에 유용

2. 완화된(비직렬성) 격리 수준

동시성 문제 (경쟁 조건)
트랜잭션이 다른 트랜잭션에서 동시에 변경한 데이터를 읽거나 두 트랜잭션이 동시에 같은 데이터를 변경하려고 할때 발생
해결 방법
전통적 - 트랜잭션 격리
직렬성 격리 - 여러 트랜잭션을 직렬적으로 실행
비직렬성 격리 수준
직렬성 격리는 성능 비용이 큼

2.1. 커밋 후 읽기 (read committed)

기본적인 특징 - 더티 읽기 방지 & 더티 쓰기 방지
read 시 커밋된 데이터만 읽음
write 시 커밋된 데이터만 덮어 씀
구현 방식 - 로우 수준 잠금
더티 쓰기 방지
특정 객체 변경 시 잠금 획득
오직 한 트랜잭션만 객체 잠금 보유 가능
더티 읽기 방지
1.
읽기 원하는 트랜잭션이 잠시 잠금 획득 후, 읽은 다음 해제
변경이 발생하였으나 커밋되지 않은 값이 있을 때, 읽기 실행되지 않음 (쓰기가 잠금을 갖고 있으므로)
현실적이지 않은 방법
읽기 트랜잭션이 쓰기 트랜잭션 대기 발생 → 응답 시간 감소 및 지연 발생
2.
과거의 값과 쓰기 잠금이 가진 새로운 값 모두 기억
쓰기 트랜잭션이 실행중 → 다른 트랜잭션은 과거값 읽음
새 값 커밋 → 다른 트랜잭션이 새로운 값 읽음
예시
cf) 더티 읽기와 더티 쓰기
더티 읽기
다른 트랜잭션에서 커밋되지 않은 데이터를 읽는 것
더티 쓰기
먼저 쓴 내용이 아직 커밋되지 않은 트랜잭션에서 쓰고, 나중에 실행된 쓰기 작업이 커밋되지 않은 값을 덮어써버리는 것
예시

2.2. 스냅샷 격리와 반복 읽기 (repeatable read)

커밋 후 읽기 (read committed)의 한계
비반복 읽기 (non-repeatable read)
읽기 스큐 /시간적 이상 현상 (read skew)
이전 질의에서 봤던 값과 다른 값을 봄
예시
기본적인 특징
각 트랜잭션은 일관된 스냅샷으로부터 데이터를 읽음
트랜잭션 시작 시, 데이터베이스에서 커밋된 상태였던 모든 데이터를 읽음
데이터가 나중에 트랜잭션에 의해 바뀌더라도, 특정 시점의 과거 데이터만 봄
오래 걸리며 읽기만 실행하는 질의에 요긴
스냅샷 격리 구현
더티 쓰기 방지 위해 쓰기 잠금 허용
쓰기 실행 시 다른 트랜잭션 접근 막음
읽는 쪽과 쓰는쪽 서로 차단 X
읽는 쪽에서 쓰는 쪽을 결코 차단하지않고 쓰는 쪽에서 읽는 쪽을 결코 차단하지 않음
다중 버전 동시성 제어 (MVCC)
여러 트랜잭션이 서로 다른 시점의 데이터베이스 상태를 봐야함
객체의 여러 버전 함께 유지
프로세스
트랜잭션 시작 시, 고유한 트랜잭션 ID 할당 받음.
데이터 쓰기 시, 쓰기를 실행한 트랜잭션 ID가 함께 붙음
created_by: 삽입한 트랜잭션 ID
deleted_by: 삭제한 트랜잭션 ID
예시
일관된 스냅샷을 보는 가시성 규칙
동작 방식
1.
데이터베이스는 각 트랜잭션 시작 시 그 시점에 진행중인 모든 트랜잭션 목록을 만듦
이 트랜잭션들이 쓴 데이터는 모두 무시 됨
커밋되더라도 무시됨
2.
어보트 된 트랜잭션이 쓴 데이터는 모두 무시 됨
3.
트랜잭션 ID다 더 큰 (현재 트랜잭션이 시작한 이후에 시작한) 트랜잭션은 커밋 여부와 관계없이 모두 무시됨
4.
그 밖에 모든 데이터는 어플리케이션 질의로 볼 수 있음

3. 갱신 손실 방지

동시 쓰기 트랜잭션 충돌 종류
더티 쓰기
갱신 손실 (lost update)
갱신 손실
애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경한 값을 다시 쓸 때 발생
read-modify-write 주기에서 발생 가능

3.1. 원자적 쓰기 연산

데이터베이스에서 제공하는 원자적 쓰기 연산 사용
read-modify-write 주기를 구현할 필요를 없애 줌
동시성 안전 (concurrency-safe)
예시
UPDATE counter SET value = value +1 WHERE key = 'foo'
몽고DB
JSON 문서 일부 지역적으로 변경 가능한 원자적 연산 제공
레디스
우선순위 큐
구현
exclusive lock으로 객체 획득하여 구현
갱신이 적용될 때 까지 다른 트랜잭션에서 객체를 읽지 못하게 함
커서 안정성 (cursor stability)

3.2. 명시적 잠금

애플리케이션에서 갱신할 객체를 명시적으로 잠금
예시
SELECT FOR UPDATE (NO WAIT)

3.3. 갱신 손실 자동 감지

병렬 실행 허용
트랜잭션 관리자
갱신 손실 발견 시 트랜잭션 어보트
read-modify-write 주기 재시도
스냅샷 격리 (repeatable read)와 결합하여 효율적 수행 가능

3.4. Compare-and-set

값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용
현재값이 이전 값과 일치하지 않으면 갱신은 반영되지 않음
read-modify-write 재시도
Where 절이 오래된 스냅샷으로부터 읽는 것을 허용한다면 갱신 손실 발생 가능
예시
UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old content';
SQL
복사

3.5. 충돌 해소와 복제

다중 리더 / 리더 없는 복제
여러 쓰기가 동시에 실행, 비동기식 복제 허용 → 최신 복사본 여러개 일 수 있음
명시적 잠금과 compare and set 사용 불가
여러개의 충돌된 버전 생성 허용 & 사후에 애플리케이션 코드 및 특별한 데이터 구조를 사용해 출돌 해제 후 버전 병합
원자적 연산은 복제 상황에서도 잘 동작함
최종 쓰기 승리 (LWW)는 갱신 손실 발생 쉬움.

4. 쓰기 스큐와 팬텀

4.1. 쓰기 스큐

쓰기 스큐 (write skew)
두 트랜잭션이 서로 다른 두 개의 다른 객체를 갱신할때 발생하는 경쟁 조건
get → update 타이밍 이슈
의사 호출 대기. 최소 1명은 대기 상태여야만 함. 두명이서 동시에 끄면? 타이밍 이슈
예시

4.2. 쓰기 스큐 갱신 손실 방지 방법

원자적 쓰기 연산 (X)
여러 객체 관련 → 단일 객체 연산은 불가
갱신 손실 자동 감지 (X)
스냅샷 격리 수준에서는 쓰기 스큐 감지불가
진짜 직렬성 격리 필요
여러 객체와 관련된 제약 조건이 필요함
대부분의 RDB는 지원 X
트리거나 구체화 뷰 활용 가능
명시적 잠금
트랜잭션 의존하는 로우를 명시적으로 잠금 해야함.

4.3. 쓰기 스큐 예시

회의실 예약 시스템
동시에 같은 회의실 중복 예약 불가
타이밍 이슈로 동시에 같은 시간대 데이터 생성 가능
직렬성 격리 필요
다중 플레이어 게임
두개의 물체를 두 플레이어가 서로 같은 위치로 옮기는 이슈
잠금으로 방지 힘듦
사용자명 획득
동시에 같은 사용자명으로 계정 생성
유일성 제약 조건
이중 사용 방지
동시에 삽입된 지불 항목 → 잔고 음수
공통 패턴
1.
어떤 조건에 해당하는 로우 GET
2.
1번의 질의 결과에 따라 애플리케이션 코드 수행
3.
Write 진행
이 쓰기 효과로 인해 2단계 결정한 전제 조건이 바뀜
팬텀 효과
어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과

4.4. 충돌 구체화 (materializing conflict)

인위적으로 데이터베이스에 잠금 객체 추가
충돌 구체화
팬텀을 데이터베이스에 존재하는 구체적인 로우 집합에대한 잠금 충돌로 변환
예시
회의실 예약의 경우 시간 슬롯과 회의실에 대한 테이블 미리 생성
각 로우는 특정 시간 범위 동안 로우 잠금
방법이 어렵고 오류 발생 가능성 높음
최후의 수단
대부분 직렬성 격리 수준을 선호

5. 직렬성

직렬성 기법
실제적인 직렬 실행
2단계 잠금
직렬성 스냅샷 격리(SSI) (낙관적 동시성 제어)

5.1. 실제적인 직렬 실행

1.
트랜잭션 하나씩만 직렬로 단일 스레드에서 실행
램 가격 저렴
OLTP 트랜잭션 - 트랜잭션이 짧고 읽기와 쓰기의 개수가 적음
2.
트랜잭션을 스토어 프로시저 안에 캡슐화 하기
상호작용식 트랜잭션은 애플리케이션과 데이터베이스 사이의 네트워크 통신에 많은 시간 소비
장점
스토어 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일 스레드에서 실행하는게 현실성 있음
I/O 대기 없음
다른 동시성 제어 메커니즘으로 오버헤드회피
단일 스레드로 좋은 처리량 획득 가능
단점
데이터베이스 벤더마다 서로 다른 언어 사용
코드 관리 어려움
테스트, 디버깅 , 모니터링 등
스토어 프로시저 잘못 사용시 사이드 임팩트가 큼
(현대에 와서는 많이 보완됨)
파티셔닝
모든 트랜잭션을 순차적으로 실행 시, 동시성 제어는 쉬움. 그러나 트랜잭션 처리량이 단일 CPU 코어 속도에 제한됨
쓰기 처리량이 높은 애플리케이션은 병목사항이 될 수 있음
파티셔닝으로 CPU 코어와 여러 노드로 확장
각 트랜잭션이 단일 파티션 내에 데이터만 읽고 쓰도록 파티셔닝 진행
여러 파티셔닝에 접근하는 트랜잭션이 있을 경우 코디네이션 필요
단일 스레드 직렬 트랜잭션 수행의 제약 조건
모든 트랜잭션은 작고 빨라야 함 (느린 트랜잭션 → 전체적 지연)
데이터셋이 메모리에 적재하는 경우로 제한
디스크는 성능 이슈
쓰기 처리량이 단일 CPU 코어에러 처리할 수 있을 만큼 낮아야 함
otherwise, 파티셔닝 필요
여러 파티션에 걸친 트랜잭션 → 엄격한 제한 필요

5.2. 2단계 잠금(PL) - 비관적 동시성 제어

정의
2단계 잠금(Two-Phase Locking, 2PL)
데이터베이스 직렬성을 구현하는데 널리 쓰이는 유일한 알고리즘
쓰기 트랜잭션 뿐만 아니라 읽기 트랜잭션도 막음
객체의 잠금을 통해 구현
cf) 스냅샷 격리 - 읽는 쪽과 쓰는 쪽은 서로를 막지 않음
잠금 종류
공유 모드 (shared mode)
독점 모드 (exclusive mode)
잠금 사용 방식
객체 읽기 원한다면 먼저 공유 모드로 잠금 획득
동시에 여러 트랜잭션 공유 모드로 잠금 획득 허용
그러나 그 객체에 독점 모드로 잠금 획득 트랜잭션 있을 경우, 완료까지 대기
트랜잭션이 쓰기를 원할 경우 독점 모드로 잠금 획득. 다른 어떠한 트랜잭션도 동시에 잠금을 획득할 수 없음
객체를 읽다가 쓰기를 실행할 때는, 공유 잠금을 독점 잠금으로 업그레이드 진행
잠금을 획득한 후에는 트랜잭션이 종료될때까지 잠금 보유 (그래서 2단계라는 이름이 붙음)
1단계: 잠금을 획득할 때
2단계: 모든 잠금을 해제할 때
잠금을 기다리는 상황을 교착 상태라고 함
2단계 잠금의 성능
트랜잭션 처리량 저하 & 질의 응답 시간 저하
원인
(주) 동시성이 줄어 듦. 트랜잭션 기다림
(부) 잠금 획득 해제 오버헤드
교착 상태 → 성능 이슈
서술 잠금 (predicate lock)
where 조건절에 명시된 조건에 대해 잠금
특정 레코드에 대한 잠금이 아니라 조건에 해당하는 모든 레코드에 잠금
아직 디비에 존재하지 않지만 미래에 추가될 수 있는 개체에도 적용 할 수 있다는 것
2단계 잠금이 서술 잠금을 포함하면 모든 형태의 쓰기 스큐와 다른 경쟁 조건을 막을 수 있어서 격리 수준이 직렬성 격리가 됨
색인 범위 잠금
서술 잠금은 현실성이 떨어짐
진행중인 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 잠금 확인에 시간 소요
색인 범위 잠금 (index-range locking, 다음 키 잠금, next-key locking)
서술 잠금을 간략하게 근사 한 것
인덱스 노드를 잠그는 방식
인덱스를 사용하는 SELECT, UPDATE, DELETE 문의 WHERE 조건절에서 사용되는 범위 조건에 대한 레코드에 잠금을 설정하는 것을 의미
간략화한 검색 조건에 색인을 넣어서 활용
서술 잠금보다 정밀하진 않지만 오버헤드가 훨씬 낮으므로 타협안이 됨
범위 잠금 적합 색인이 없다면 테이블 전체에 공유 잠금 대체. (대비책)

5.3. 직렬성 스냅샷 격리 (SSI, Serializable Snapshot Isolation)

동시성 제어
직렬성
2단계 잠금 - 성능 안좋음
직렬 실행 - 확장성 낮음
비직렬성 (완화된 격리 수준)
스냅샷 격리, 쓰기후 읽기
성능 좋음. 다양한 경쟁 조건(갱신 손실, 쓰기스큐, 팬텀)에 취약
비관적 동시성 제어
뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질때까지 기다림
예시 - 2단계 잠금
낙관적 동시성 제어
위험한 상황이 발생 가능성이 있을 때 트랜잭션을 막는 대신 모든 것이 괜찮아 질 거라는 희망을 갖고 계속 진행
트랜잭션에서 실행되는 모든 읽기는 데이터베이스의 일관된 스냅샷을 보게 됨
스냅샷 격리 위에 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트 시킬 트랜잭션을 결정하는 알고리즘 추가
예시 - 직렬성 스냅샷 격리
뒤처진 전제에 기반한 결정
스냅샷 격리 하에서는 트랜잭션이 커밋되는 시점에 원래 질의의 결과가 더이상 최신이 아닐 수도 있음
데이터베이스가 어떻게 질의 결과가 바뀌었는지 알 수 있을까?
두가지 상황 고려 필요
오래된 (stale) MVCC 객체 버전을 읽었는지 감지 (읽기 전 커밋되지 않은 쓰기 발생)
과거의 읽기에 영향을 미치는 쓰기 감지 (읽은 후에 쓰기 실행됨)
오래된 MVCC 읽기 감지하기 - 읽기 전 커밋되지 않은 쓰기 확인
스냅샷 격리는 다중 버전 동시성 제어(MVCC)로 구현됨
다른 트랜잭션에서 동일 레코드의 데이터가 변경된 경우 이슈 발생
이상 현상 방지 하기 위해서 데이터베이스는 트랜잭션이 MVCC 가시성 규칙에 따라 다른 트랜잭션의 쓰기를 무시하는 경우는 추적해야함.
커밋 시 디비에 무시된 쓰기중 커밋된 게 있다면 확인
커밋된게 있다면 트랜잭션 어보트해야함
예시
과거의 읽기에 영향을 미치는 쓰기 감지하기 - 읽은 후에 다른 트랜잭션이 그 데이터를 변경할 때
트랜잭션이 디비에 쓸때 영향받는 데이터를 최근에 읽은 트랜잭션이 있는지 색인에서 확인
충돌되는 쓰기가 존재할 경우 어보트 진행
예시
직렬성 스냅샷 격리의 성능
2단계 잠금에 비해 잠금을 기다릴 필요가 없음
읽기 작업 부하가 심할 경우 장점이 있음
순차 실행에 비해 단일 CPU 코어 처리량에 제한되지 않음

6. 정리

격리 수준
커밋 후 읽기 (read commited)
읽기 스큐 발생 가능
스냅샷 격리 (repeatable read)
팬텀 읽기 발생 가능
직렬성 격리
실제적 직렬성 구현
2PL
직렬성 스냅샷 격리 (Serializable)
경쟁 조건 이상 현상
더티 읽기 - 커밋 되지 않은 데이터 읽기
더티 쓰기 - 커밋되지 않은 데이터를 덮어쓰기
읽기 스큐 (비반복 읽기)
다른 시점에 다른 데이터 보기 → MVCC
이전과 다른 결과가 나타나는 현상
쓰기 스큐
트랜잭션이 무언가를 읽고, 읽은 값 기반으로 어떤 결정을 하여 데이터베이스를 씀.
참이 아닐 수도 있음
크아 방에 들어가기.진입 시도 → 실패
직렬성 격리만 막을 수 있음
갱신 손실
두 클라이언트가 동시에 read-modify-write 주기 실행
한 트랜잭션이 다른 트랜잭션의 변경을 포함하지 않은 채로 다른 트랜잭션의 쓴 내용을 덮어써서 데이터가 손실됨
수동 잠금 (SELECT FOR UPDATE)으로 해결
팬텀 읽기
트랜잭션이 어떤 객체 읽음
다른 클라이언트가 해당 검색 결과에 영향주는 쓰기 실행
팬텀 발생
cf) 팬텀 읽기와 읽기 스큐의 차이
팬텀 읽기 - 새로운 행이 추가되는 경우 발생
읽기 스큐 - 두개 이상의 트랜잭션이 서로 영향을 미치는 상황
cf) 갱신 손실과 쓰기 스큐의 차이
갱신 손실 - 데이터를 갱신하는 결과에 대한 일관성 유지 X
쓰기 스큐 - 데이터를 갱신하는 과정에서 발생하는 문제
직렬성 격리
완화된 격리 수준은 이상현상 일부는 막아주나, 나머지는 개발자가 수동 처리해야함(명시적잠금)
직렬성 격리만 이 모든 문제를 해결함
직렬성 트랜잭션 구현 종류
말 그대로 트랜잭션 순서대로 실행
2단계 잠금
직렬성 스냅샷 격리 (SSI) - SERIALIZABLE

스터디

1. 질문만들기

Harry

팬텀 효과란 무엇인가요?
팬텀 읽기와 읽기 스큐의 차이가 뭔가요?
쓰기 스큐와 갱신 손실의 차이가 무엇인가요?
각 격리 수준에서 발생 가능한 경쟁 조건 이상 현상에 대해 서술해주세요
직렬성 기법의 종류에대해 알려주세요
서술 잠금과 2PL, 색인 잠금의 차이는 무엇인가요?
갱신 손실 방지 방법들엔 무엇이 있나요?

Matthew

갱실 손실이란 무엇인가요?
더티 읽기와 더티 쓰기는 무엇인가요?
동시성 문제를 해결하기 위한 방법을 대분류 기준으로 나열 해주세요

2. 핵심 질문에 답하기

3. 책 같이 살펴보기

240P, 일관된 스냅숏 보는 가시성 규칙, 이거 너무 heavy 하지 않음? DB 입장서
어보트된 트랜잭션이란건 어찌 알까?
3번, 무시 되는 이유는 이전 트랜잭션이 커밋 되지 않았으므로?
241P, 다중 버젼 데이터 베이스, 이게 뭐여
263P, 두번째 단락
트랜잭션이 커밋하려고 할 때 데이터베이스는 무시된 쓰기 중에 커밋된게 있는지 확인해야한다 → 무시된 쓰기란 무엇이며 어떻게 구분?