on
[데이터중심애플리케이션설계] 7. 트랜잭션
[데이터중심애플리케이션설계] 7. 트랜잭션
트랜잭션이란?
애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법이다.
개념적으로 한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행된다.
트랜잭션이 필요한 이유는 하나의 기능을 하는 비즈니스 로직 수행중에 시스템 다운 등 예상치 못한 장애가 발생하는 경우 부분적 실패에 대해 고려하지 않기 위함이다.
ACID 의 의미
데이터베이스의 내결함성 메커니즘을 나태나는 정확한 용어를 확립하기 위해 ACID를 만들었다.
원자성
여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶여 있는데 결함 때문에 완료될 수 없다면, 어보트되고 데이터베이스는 이 트랜잭션에서 지금까지 실행한 쓰기를 무시하거나 취소해야 한다.
일관성
애플리케이션 단에서 데이터에 관한 불변식을 선언하고 이를 확인하여 보장하는 속성이다.
데이터베이스의 외래키 제약조건, 유일성 제약조건 등을 통해 보장할 수 있으나 일반적으로 일관성은 애플리케이션단에서 구현된다.
격리성
동시에 실행되는 트랜잭션은 서로 격리된다는 것을 의미한다.
실제로는 성능손해 때문에 직렬성보다 보장이 약한 스냅숏 격리를 구현한다.
http://www.bailis.org/blog/hat-not-cap-introducing-highly-available-transactions/
지속성
트랜잭션이 성공적으로 커밋됐다면, 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에 기록한 모든 데이터는 손실되지 않는다는 보장이다.
복제 기능이 있는 디비에서 지속성은 데이터가 다른 노드에도 복사되었다는 것을 의미하기도 한다. 쓰기 전 로그 등을 통해 구현한다.
트랜잭션 격리 수준
1. 커밋 후 읽기
더티 읽기와 더티 쓰기 방지
더티 읽기는 한 트랜잭션에서 커밋 되지 않은 데이터를 다른 트랜잭션에서 읽는 것을 말한다.
더티 쓰기는 한 트랜잭션에서 아직 커밋하지 않은 값을 다른 트랜잭션에서 덮어 쓰는 것을 말한다.
더티 읽기는 왜 안좋은 것일까?
더티 읽기를 허용하면 사용자에게 일관되지 않은 데이터를 보여줄 수 있다. 이는 다른 트랜잭션이 잘못된 결정을 하는 원인이 될 수도 있다.
트랜잭션이 어보트 될 경우 더티 읽기를 허용하면 이를 읽은 다른 트랜잭션까지도 어보트 시켜야한다. 끔찍쓰..
방지법
더티 읽기는 과거에 커밋된 값과 현재 쓰기 잠금을 가지고 있는 트랜잭션이 쓴 값 모두를 저장하고, 다른 트랜잭션에게 커밋 전에는 과거값을 제공한다.
더티 쓰기는 먼저 쓴 트랜잭션이 커밋, 어보트 될 때까지 나중에 들어온 트랜잭션을 지연시킨다.
커밋 후 읽기 구현
더티 쓰기는 디비 수준의 잠금을 통해 구현된다. 어떤 값을 변경하고 싶으면 해당 값에 대한 lock을 획득해야 한다. 이는 대부분의 디비에서 커밋 후 읽기 모드일 경우 자동 실행 된다.
더티 읽기는 잠금을 통해 구현할 경우 쓰기 트랜잭션의 잠금으로 인해 lock 획득을 위해 오랫동안 대기할 수 있으므로, 잠금을 사용하지 않고 위 방지법에서 설명한 방법으로 구현한다.
커밋 후 격리의 문제점
아래와 같이 한 트랜잭션에서 두 객체에 접근해 값을 얻으려고 할때, 그 사이에 두 객체에 update가 발생하면 잘못된 결과값을 얻을 수 있다. 이를 읽기 스큐 라고한다.
핵심은 한 트랜잭션의 두 읽기 사이에 다른 트랜잭션의 업데이트가 커밋될때 발생한다!!
스냅숏 격리와 반복 읽기
위 문제들은 각 트랜잭션이 다른 시점의 데이터 베이스를 볼 수 있기 때문에 발생한다.
스냅숏 격리는 각 트랜잭션이 데이터베이스의 일관된 스냅숏(시점)으로부터 읽는다.
구현
더티 쓰기 방지를 위해 쓰기 잠금을 사용한다.
더티 읽기 방지를 위해서는 아무 잠금도 하지 않는다.
데이터베이스는 객체마다 커밋된 버전 여러개를 유지할 수 있어야 한다. 진행중인 여러 트랜잭션이 서로 다른 시점의 데이터베이스 상태를 봐야할 수도 있기 때문이다.
이 기법을 다중 버전 동시성 제어(MVCC) 라고 한다.
PostgresSQL에서 구현 방식
1. 트랜잭션이 시작하면 계속 증가하는 고유한 트랜잭션 ID를 할당 받는다.
2. 데이트럴 쓸 때마다 쓰기를 실행한 트랜잭션 ID가 함께 붙는다.
3. 테이블의 각 로우에는 트랜잭션 ID를 저장하는 created_by, deleted_by 필드가 있다.
4. 갱신은 내부에서 삭제와 생성으로 변환된다.
그렇다면 어떻게 일관된 스냅샷을 보도록 할까?!!
정답은 트랜잭션 ID 기준으로 필터링(쳐낼 수 있는것은 모두 쳐낸다)하는 것!
1. 데이터베이스는 각 트랜잭션이 시작할때 그 시점에 진행중인(커밋이나 어보트 전) 모든 트랜잭션의 목록을 만든다. 이 트랜잭션들이 쓴 데이터는 모두 무시된다. 데이터를 쓴 트랜잭션이 나중에 커민되어도 무시한다.
2. 어보트된 트랜잭션이 쓴 데이터 무시한다.
3. 트랜잭션 ID가 더 큰 트랜잭션이 쓴 데이터는 그 트랜잭션의 커밋 여부에 관계없이 모두 무시한다.
4. 그 밖의 모든 데이터는 질의로 볼 수 있다.
지금까지는 주로 동시에 실행되는 쓰기 작업이 있을때, 읽기 트랜잭션이 어떤 데이터를 보고 이때 발생하는 문제와 해결법에 대해 이야기 했다.
그렇다면!! 이제부터 동시 쓰기시 발생할 수 있는 문제들에 대해 알아보자.
갱신 손실
갱신 손실은 어떤 값을 읽고, 갱신하고, 갱신한 값을 쓸때 발생할 수 있다. 이 작업이 두 트랜잭션에서 동시에 발생한다면, 나중에 실행된 질의는 엎어쳐질 수 있다.
해결법
1. 원자적 쓰기 연산
데이터 베이스에서 제공하는 원자적 갱신을 사용한다.
Ex) UPDATE counters SET value = value + 1 WHERE key = 'foo'
원자적 연산은 보통 객체를 읽을 때 그 객체에 독점적인 lock 을 획득해서 구현한다.
2. 명시적인 잠금
애플리케이션 단에서 명시적 잠금 쿼리로 질의한다.
3. 데이터베이스에서 갱신 손실 자동 감지
데이터베이스가 갱신손실이 발생한 트랜잭션을 감지하고 이를 자동으로 어보트 시킨다.
4. Compare and Set
업데이트 하기전에 get을 한번 해서 이전값과 현재값이 같은경우에만 업데이트 한다.
Ex) UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old content'
위 방법들은 replication하고 있지 않은 디비에만 적용할 수 있다. replication을 제공하는 데이터 베이스는 일반적으로 비동기식 복제, 여러 쓰기가 동시에 실행되는것을 허용하기 때문에 데이터의 최신본이 하나라고 보장할 수 없다.
replication하고 있는 경우 해결책은??
충돌된 버전을 모두 유지하고 애플리케이션에게 충돌을 해소하고 이 버전들을 병합하게 하는 것 ㅠ
쓰기 스큐와 팬텀
두 트랜잭션이 두개의 다른 객체를 동시에 갱신하는 과정에서 발생하는 동시성 문제를 쓰기 스큐 라고 한다.
(쿼리 수행 조건이 최신 데이터 기반으로 판단되지 않아서 발생)
더티쓰기나 갱신손실같은 경우는 동일한 객체를 갱신할때 발생한다.
어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과를 팬텀이라고 한다.
스냅샷 격리는 읽기 전용 질의에서는 팬텀을 회피하지만 읽기-쓰기 트랜잭션에서는 팬텀이 쓰기 스큐의 까다로운 경우를 유발할 수 있다.
찾아보기
다이나모 db에서 색인에 대한 일관성 보장은 어떻게 하는가?
from http://surmong.tistory.com/10 by ccl(A) rewrite - 2021-09-24 11:00:41