1. 트랜잭션
1.1 정의
- 어플리케이션 내에서 복수의 읽기와 복수의 쓰기를 하나의 논리적 단위로 묶는 방법을 의미한다.
- 한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행 ⇒ 전체 성공/전체 실패
1.2 목적
- 프로그래밍 모델을 단순화하려는 목적
- 동시성 관련 문제를 어플리케이션단에서 푸는게 아닌 데이터베이스에서 해결 ⇒ 어플리케이션에서는 잠재적인 오류 시나리오와 동시성 문제에 대해 무시 가능
1.3 ACID
트랜잭션은 아래와 같은 것을 보장해야한다고 알려져 있음(ACID Compliant)
- 원자성(Atomicity)
- 전체 성공/전체 실패만 가능하다.
- 일관성(Consistency)
- 트랜잭션 수행 전, 후에 데이터 모델의 모든 제약 조건을 만족해야 함
- 격리성(Isolation)
- 동시에 실행되는 트랜잭션은 격리된다.
- 지속성(Durability)
- 한번 반영된 트랜잭션은 유지된다.
- 노노
원자성과 격리성은 한 트랜잭션 내에서 여러번의 쓰기를 했을 때 데이터베이스가 어떤식으로 동작해야하는지 정의
다중 객체 트랜잭션은 어떤 읽기 연산과 쓰기 연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 있어야함
관계형 데이터 베이스에서는 데이터 베이스 - 서버 사이의 TCP 연결을 기반
비관계형 데이터 베이스에서는 반드시 트랜잭션 시멘틱을 지원하지는 않음
직렬성 격리는 성능 비용이 상당함
따라서 완화된 격리 수준을 사용하는 시스템이 흔함
완화된 격리 수준으로 인해 발생한 문제
- 금전적인 손실
- 재무 감사원의 조사
- 고객 데이터 오염
2. 트랜잭션 격리 수준
- 트랜잭션 격리 수준은 트랜잭션 사이의 데이터 가시성 정도를 나타내는 것
2.1 커밋 후 읽기(Read Committed)
- 트랜잭션이 격리수준(read uncommitted) 이 없다고 가정했을 때 발생할 수 있는 동시성 문제
- 더티 읽기
- 더티 쓰기
2.1.1 커밋 후 읽기
- 커밋된 데이터만 쓰거나 읽을 수 있음
⇒
더티 읽기와더티 쓰기를 막아줌- Dirty Read
더티 쓰기: 두 트랜잭션이 데이터 베이스에 있는 동일한 객체를 동시에 갱신하려고 했을 때 발생하는 문제 ex) 중고차 판매 웹 사이트에서 두 사람(앨리스, 밥) 이 동시에 같은 차를 사는 경우- 판매 목록에 구매자를 업데이트하는 연산
- 송장을 업데이트 하는 연산
⇒더티 쓰기를 막지 않으면 구매자는 밥, 송장은 앨리스로 될 수 있음
2.1.2 구현 방법
- 로우 수준 잠금
- 로우를 변경하는 트랜잭션이 끝나기 전까지 읽기를 막음 ⇒ 읽기 잠금의 경우 응답 시간에 큰 영향을 미침
- 커밋 후 읽기
- 과거에 커밋된 값과 새로운 트랜잭션의 쓰기 값을 모두 기억한다. 트랜잭션이 진행중인 동안에는 과거의 값을 읽고 새 값이 커밋되면 다른 트랜잭션이 새 값을 읽을 수 있음
2.1.3 Read Committed 에서 발생가능한 동시성 문제
- 영례는 은행에 1,000달러의 저축이 있고 두 계좌에 500달러씩 나눠 놓았음
- 계좌1 에서 2로 100달러를 전송하는 트랜잭션을 실행
- 만약 그녀가 운이 없어 서 트랜잭션이 처리되고 있는 순간에 계좌 잔고를 보게 되면
- 계좌2는 입금이 되기 전 상태(잔고가 500달러있는)를 보고 계좌1은 출금이 된 후 상태(잔고가400달러로바뀐)를 볼 수 있음
- 영례에게는 현재 계좌 총액이 900달러만 있는 것처럼 나온다. 100달러는 연기처럼 사라져 버린 것 처럼보인다. ⇒ 비반복 읽기(nonrepeatable read) or 읽기 스큐(read skew) ⇒ 스냅숏 격리 수준으로 해결
2.2 스냅숏 격리(Snapshot Isolation)
“”” 데이터 갱신 시점을 기록해 트랜잭션 시점과 비교해 데이터를 보여주는 방식”””
- 지원하는 DB마다 부르는 이름이 다름 ⇒ 오라클에서는 Serializable, Postgresql, mysql에서는 Repeatable Read
- 더티 쓰기를 방지하기 위해 쓰기 잠금을 사용한다.
- 쓰기는 쓰기만 차단한다.
2.2.1 MVCC(Multi Version Concurrency Control, MVCC)
-
커밋 후 읽기의 일반화된 방법
-
커밋후 읽기는 쿼리마다 독립된 스냅샷을 사용
- 쿼리마다 커밋전, 커밋 후 값을 갖는다.
-
스냅샷 격리는 하나의 트랜잭션에 대해 동일한 스냅숏(트랜잭션 Id, 트랜잭션 시점) 활용
-
알고리즘
-
트랜잭션이 시작하면 계속 증가하는 트랜잭션 ID를 할당 받음
-
테이블의 각 로우에는 그 로우를 테이블에 삽입한 트랜잭션의 ID를 갖는
created_by필드와 처음에는 비어 있는deleted_by필드도 있음- 생성과 삭제를 나타내는 플래그 필드
-
트랜잭션이 로우를 삭제하면 실제로 데이터베이스에서 지우지 않고
deleted_by필드를삭제를 요청한 트랜잭션의 ID로 설정함으로써 지워졌다고 표시한다.- 아무 트랜잭션도 삭제된 데이터에 접근하지 않는게 확실해지면 데이터베이스의 GC 프로세스가 지워졌다고 표시된 로우들을 삭제해서 사용량을 줄임
-
갱신은 내부에서 삭제와 생성으로 변환됨.
-
위 그림에서
트랜잭션 13은계좌 2에서 100달러를 출금하는 트랜잭션 -
이 TR로 인해 잔고 가 500달러에서 400달러로 바뀐다.
-
이제 accounts 데이터베이스에서 계좌 2는 실제로 두 개의 로우를 갖게 된다.
created_by deleted_by amount 1 3 13 500 2 13 null 400 - 트랜잭션 13이 삭제한 것으로 표시한 잔고가 500달러인 로우
- 트랜잭션 13이 생성한 잔고가 400달러인 로우다.
-
스냅숏 격리 가시성 규칙
- 하나의 트랜잭션은 DB에서 객체를 읽을 때 트랜잭션 ID을 기준으로 어떤것은 읽을 수 있고 어떤것은 읽을 수 없음
- 하나의 트랜잭션이 시작할 때 진행중(커밋, 어보트 되지 않은)인 모든 트랜잭션을 가져온다. 이 트랜잭션들이 쓰는 데이터는 모두 무시
- 어보트 된 트랜잭션이 쓴 데이터는 무시
- 트랜잭션 ID가 큰 트랜잭션이 쓴 아이디는 무시됨
- 위 1~3을 제외하면 모두 읽을 수 있음
- 색인
- MVCC DB에서 색인은 어떻게 동작하는가?
- 색인이 객체의 모든 버전을 가리키고 색인 질의를 통해 오래된 버전을 필터링 구현 세부 사항
- postgresql
- 동일 객체의 다른 버전이 같은 페이지에 저장될 수 있으면 색인 갱신을 회피하는 최적화
- CouchDB, Daytomic, LMDB에서는 추가 전용 B 트리를 사용
- (append-only/copy-on-write) 변종 사용
- MVCC DB에서 색인은 어떻게 동작하는가?
-
2.2.2 발생할 수 있는 동시성 문제
커밋후 읽기와스냅샷 격리는 동시에 실행되는 쓰기 작업이 있을 때 다른 읽기 전용 트랜잭션이 무엇을 볼 수 있는지에 대한 격리 수준- 쓰기-쓰기 충돌 문제중에서는
더티 쓰기에 대해서만 이야기 했음 더티 쓰기외에도 아래와 같은 것들이 있음갱신 손실쓰기 스큐
-
갱신 손실(lost update)
- DB에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때 발생(read-modify-write)
- 두 트랜잭션(modify, write)가 동시에 발생하면 하나는 손실될 수 있음
- 카운터 증가, 계좌 잔고 갱신
- 트랜잭션 발생 순서에 따라 결과가 달라짐
- 두명의 사용자가 동시에 같은 페이지 편집
- 카운터 증가, 계좌 잔고 갱신
- 해결책
- 원자적 쓰기 연산
- 객체에 독점적인 잠금을 획득해서 구현 ⇒ 커서 안정성
- 모든 원자적 연산을 단일 스레드에서 실행되도록 강제
- 명시적인 잠금
- DB에서 원자적 연산을 제공하지 않을 때 어플리케이션 단에서 명시적으로 잠금
- 갱신 손실 자동 감지
- DB내부의 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 어보트 시키고 read-modify-write 주기를 재시도하도록 강제하는 방법
- 병렬 실행을 통해 성능에서 이득을 본다.
- PostGresql repeatable Read, Oracle Serializable, SQL Server 스냅샷 격리 수준에서는 지원
- MYSQL/InnoDB Repeatable Read에서는 지원하지 않음
- 원자적 쓰기 연산
-
쓰기 스큐(write skew) 예시)
- 최소 한 명의 의사는 대기상태로 있어야하는 호출 관리 어플리케이션
- 앨리스와 밥이 호출 대기 상태에 있다가 거의 동시에 호출 대기 상태 종료 버튼을 누름
- 각 트랜잭션에서 어플내에 대기중인 의사의 수가 2명인지 확인함, 2명 존재 빠져도 괜찮다고 판단
- 두 트랜잭션 모두 성공해서 대기중인 의사가 없게된다.
- 위의 예시에서 두 트랜잭션이 차례대로 일어났다면 요구 사항을 만족 시켰을 것임
-
여러 객체가 관련되므로 원자적 단일 객체 연산을 도움되지 않음
-
갱신 자동 손실 감지도 도움이 안됨
-
특정 데이터 베이스에서는 제약 조건을 걸 수 있음
-
Row를 잠그는 방법
BEGIN TRANSACTION; SELECT * FROM doctors where on_call = true AND shift_id=1234 FOR UPDATE UPDATE doctors SET on_call = false WHERE name = "ALICE" AND shift_id = 1234; COMMIT;
-
쓰기 스큐와 관련된 다른 예시
- 회의실 예약 시스템
- 사용자 명
- 이중 사용 방지
-
위에 열거한 예시는 다음과 같은 패턴을 갖는다.
- SELECT 질의가 어떤 검색 조건에 부합하는 로우를 검색해 특정한 요구사항을 만족하는지 확인 ⇒ 호출 대기인 의사가 2명 이상인지 확인하는 쿼리
- 질의 결과에 따라 어플리케이션 코드가 어떤식으로 진행할지 결정 ⇒ 2명 이상이면 자신의 호출 대기를 해제 할 수 있음
- 계속 처리하기로 결정했다면 DB에 쓰고 트랜잭션을 커밋
- 이 트랜잭션으로 인해 2번의 조건이 변경됨 ⇒ 자신으 호출 대기 해제, 1번 쿼리에 변경이 일어남
-
이처럼 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 쿼리 결과를 바꾸는 효과를
팬텀이라고 한다. -
해결 방법