ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장. 트랜잭션
    STUDY/데이터 중심 애플리케이션 설계 2025. 8. 19. 22:22

    데이터 시스템의 여러 가지 문제

    • 데이터베이스 소프트웨어나 하드웨어는 (쓰기 연산이 실행 중일 때를 포함해서) 언제라도 실패할 수 있다.
    • 애플리케이션은 (연속된 연산이 실행되는 도중도 포함해서) 언제라도 죽을 수 있다.
    • 네트워크가 끊기면 애플리케이션과 데이터베이스의 연결이 갑자기 끊기거나 데이터베이스 노드 사이의 통신이 안 될 수 있다.
    • 여러 클라이언트가 동시에 데이터베이스에 쓰기를 실행해서 다른 클라이언트가 쓴 내용을 덮어쓸 수 있다.
    • 클라이언트가 부분적으로만 갱신돼서 비정상적인 데이터를 읽을 수 있다.
    • 클라이언트 사이의 경쟁 조건은 예측하지 못한 버그를 유발할 수 있다.

     

    트랜잭션

    • 애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법으로, 위에서 발생하는 문제를 단순화하는 메커니즘으로 채택돼 왔다.
    • 한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행된다.
    • 전체가 성공(커밋)하거나 실패(어보트/abort, 롤백)한다.
    • 부분적인 실패를 걱정할 필요가 없다.
    • 현대의 거의 모든 관계형 데이터베이스와 일부 비관계형 데이터베이스는 트랜잭션을 지원한다. 

     

    ACID

    • 트랜잭션이 제공하는 안전성 보장은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 의미하는 ACID로 잘 알려져 있다.
    • 데이터베이스에서 내결함성 메커니즘을 나타내는 용어로 만들어졌지만, 현실에서는 데이터베이스마다 ACID 구현이 제각각이다.
    • ACID 표준을 따르지 않는 시스템은 때로 BASE라고 불리는데, 기본적으로 가용성을 제공하고(Basically Available), 유연한 상태를 가지며(Soft state), 최종적 일관성(Eventual consistency)을 지닌다는 뜻이다.
    • 원자성(Atomicity)
      • 여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶여 있어 결함 때문에 완료(커밋)될 수 없다면 어보트(abort)되고 데이터베이스는 이 트랜잭션에서 지금까지 실행한 쓰기를 무시하거나 취소해야 한다.
      • 트랜잭션 내의 모든 연산들은 전체가 정상적으로 수행이 완료되거나 아니면 어떠한 연산도 수행되지 않아야 한다.
    • 일관성(Consistency)
      • 트랜잭션 전후의 데이터베이스는 항상 일관된 상태여야 한다.
    • 격리성(Isolation)
      • 동시에 실행되는 트랜잭션은 서로 격리되어야 한다. 즉, 동시에 실행되는 여러 트랜잭션은 서로 영향을 주지 않고 독립적으로 실행되는 것처럼 보여야 한다.
      • 데이터베이스는 여러 트랜잭션이 동시에 실행됐더라도 트랜잭션이 커밋됐을 때의 결과가 트랜잭션이 순차적으로 실행됐을 때의 결과와 동일하도록 보장한다.
    • 지속성(Durability)
      • 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되는 않는 것을 보장한다.
      • 단일 노드 데이터베이스에서 지속성은 일반적으로 데이터가 하드디스크나 SSD 같은 비휘발성 메모리에 기록됐다는 의미이다.

     

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

    • ACID에서 원자성과 격리성은 클라이언트가 한 트랜잭션 내에서 여러 번의 쓰기를 하면 데이터베이스가 어떻게 해야하는지 알려준다.
      • 원자성 : 쓰기를 이어서 실행하는 도중 오류가 발생하면 트랜잭션은 어보트돼야 하고 그때까지 쓰여진 내용은 폐기돼야 한다. 즉, 데이터베이스는 전부 반영되거나 아무것도 반영되지 않는 것을 보장함으로써 부분 실패를 걱정할 필요가 없게 한다.
      • 격리성 : 동시에 실행되는 트랜잭션들은 서로를 방해하지 말아야 한다. 예를 들어, 한 트랜잭션이 여러 번 쓴다면 다른 트랜잭션은 그 내용을 전부 볼 수 있든지 아무것도 볼 수 없든지 둘 중 하나여야 하고 일부만 볼 수 있어서는 안 된다.
      • 격리성 위반 : 트랜잭션이 다른 트랜잭션에서 썼지만 커밋되지 않은 데이터를 읽음(dirty read)
    • 다중 객체 트랜잭션
      • 어떤 읽기 연산과 쓰기 연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 필요한데, 관계형 데이터베이스에서는 클라이언트와 데이터베이스 서버 사이의 TCP 연결을 기반으로 한다.
      • 어떤 특정 연결 내에서 BEGIN TRANSACION 문과 COMMIT 문 사이의 모든 것은 같은 트랜잭션에 속하는 것으로 여겨진다.
      • 비관계형 데이터베이스는 이런 식으로 연산을 묶는 방법이 없는 경우가 많아, 어떤 연산은 성공하고 나머지 연산은 실패해도 데이터베이스가 부분적으로 갱신된 상태가 될 수 있다.
    • 단일 객체 쓰기
      • 저장소 엔진들은 거의 보편적으로 한 노드에 존재하는 (키-값 쌍 같은) 단일 객체 수준에서 원자성과 격리성을 제공하는 것을 목표로 한다.
      • 원자성은 장애 복구용 로그를 써서 구현할 수 있고, 격리성은 각 객체에 잠금을 사용해(동시에 한 스레드만 객체에 접근하도록) 구현할 수 있다.
      • 여러 클라이언트에서 동시에 같은 객체에 쓰려고 할 때 갱신 손실을 방지하므로 유용하지만, 일반적으로 쓰이는 의미의 트랜잭션은 아니다.
    • 오류와 어보트 처리
      • 트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도할 수 있다는 것이다.
      • 데이터베이스가 원자성, 격리성, 또는 지속성 보장을 위반할 위험이 있으면 트랜잭션이 절반 정도 완료된 상태에 머물게 하는 대신 트랜잭션을 완전히 폐기한다.
      • 어보트된 트랜잭션을 재시도하는 것은 간단하고 효과적인 오류 처리 매커니즘이지만 완벽하지는 않다.

     

    완화된 격리 수준

    • 동시성 문제(경쟁 조건)는 트랜잭션이 다른 트랜잭션에서 동시에 변경한 데이터를 읽거나 두 트랜잭션이 동시에 같은 데이터를 변경하려고 할 때 나타난다.
    • 데이터베이스는 오랫동안 트랜잭션 격리를 제공함으로써 애플리케이션 개발자들에게 동시성 문제를 감추려고 했다.
    • 직렬성 격리는 데이터베이스가 여러 트랜잭션들이 직렬적으로 실행되는 것(즉 동시성 없이 한 번에 트랜잭션 하나만 실행)과 동일한 결과가 나오도록 보장한다는 것을 의미한다.
    • 하지만 직렬성 격리는 성능 비용이 있기 때문에, 완화된 격리 수준을 사용하는(어떤 동시성 이슈로부터는 보호하지만, 모든 이슈로부터는 보호하지 않는) 시스템들이 흔하다.

     

    커밋 후 읽기

    • 가장 기본적인 수준의 트랜잭션 격리는 커밋 후 읽기(read committed)이다.
    • 두 가지를 보장
      • 데이터베이스에서 읽을 때 커밋된 데이터만 보게 된다(더티 읽기가 없음).
      • 데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 된다(더티 쓰기가 없음).
    • 더티 읽기 방지
      • 더티 읽기는 다른 트랜잭션에서 커밋되지 않는 데이터를 보는 경우를 의미한다.
      • 커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티 읽기를 막아야 한다.
      • 트랜잭션이 쓴 내용은 커밋된 후에야 다른 트랜잭션에게 보여야 한다.
    • 더티 쓰기 방지
      • 더티 쓰기는 나중에 실행된 쓰기 작업이 커밋되지 않은 값(먼저 실행된 쓰기 작업)을 덮어써버리는 경우를 의미한다.
      • 커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티 쓰기를 막아야 한다.
      • 보통 먼저 쓴 트랜잭션이 커밋되거나 어보트될 때까지 두 번째 쓰기를 지연시키는 방법을 사용한다.
    • 구현 방법
      • 로우 수준 잠금을 사용해 더티 쓰기를 방지한다.
      • 트랜잭션에서 특정 객체(로우나 문서)를 변경하고 싶다면 먼저 해당 객체에 대한 잠금을 획득해야 한다. 그리고 트랜잭션이 커밋되거나 어보트될 때까지 잠금을 보유하고 있어야 한다. 오직 한 트랜잭션만 어떤 주어진 객체에 대한 잠금을 보유할 수 있다.
      • 더티 읽기를 방지하는 방법으로는 쓰여진 모든 객체에 대해 데이터베이스가 과거에 커밋된 값과 현재 쓰기 잠금을 갖고 있는 트랜잭션에서 쓴 새로운 값을 모두 기억하고, 해당 트랜잭션이 실행 중인 동안 그 객체를 읽는 다른 트랜잭션들은 과거의 값을 읽게 하는 것이다.

     

    스냅숏 격리와 반복 읽기

    • 커밋 후 읽기 격리 수준을 사용하더라도 동시성 버그가 생길 수 있는 경우가 많다.
    • ex) 비반복 읽기(monrepeatable read)나 읽기 스큐(read skew), 일관성이 깨진 상태인 데이터베이스를 보게되는 경우
    • 일시적인 비일관성이 문제가 되는 경우
      • 백업 : 백업 프로세스가 실행되는 동안에도 계속 데이터베이스에 쓰기가 실행되기 때문에, 백업의 일부는 데이터의 과거 버전을 다른 부분은 새 버전을 갖게 된다. 이러한 백업을 사용해서 복원하면 비일관성이 영속적이게 된다.
      • 분석 질의와 무결성 확인
    • 스냅숏 격리
      • 위 문제의 가장 흔한 해결책
      • 각 트랜잭션은 데이터베이스의 일관된 스냅숏으로부터 읽는다.
      • 즉 트랜잭션은 시작할 때 데이터베이스에 커밋된 상태였던 모든 데이터를 본다.
      • 데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 보게 된다.
      • 백업이나 분석처럼 실행하는 데 오래 걸리며 읽기만 실행하는 질의에 유용하다.
    • 스냅숏 격리 구현
      • 커밋 후 읽기 격리처럼 전형적으로 더티 쓰기를 방지하기 위해 쓰기 잠금을 사용하고, 읽을 때는 잠금이 필요하지 않다.
      • 핵심 원리는 읽는 쪽에서 쓰는 쪽을 결코 차단하지 않고 쓰는 쪽에서 읽는 쪽을 결코 차단하지 않는다는 것이다. 데이터베이스는 잠금 경쟁 없이 쓰기 작업이 일상적으로 처리되는 것과 동시에 일관성 있는 스냅숏에 대해 오래 실행되는 읽기 작업을 처리할 수 있다.
      • 데이터베이스는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 한다. => 다중 버전 동시성 제어(multi-version concurrency control, MVCC)
      • 커밋 후 읽기는 질의마다 독립된 스냅숏을 사용하고, 스냅숏 격리는 전체 트랜잭션에 대해 동일한 스냅숏을 사용한다.
    • 일관된 스냅숏을 보는 가시성 규칙
      • 트랜잭션은 데이터베이스에서 객체를 읽을 때 트랜잭션 ID를 사용해 어떤 것을 볼 수 있고 어떤 것을 볼 수 없는지 결정한다.
        1. 데이터베이스는 각 트랜잭션을 시작할 때 그 시점에 진행 중인 모든 트랜잭션의 목록을 만든다. 이 트랜잭션들이 쓴 데이터는 모두 무시된다. 설령 데이터를 쓴 트랜잭션이 나중에 커밋되더라도 마찬가지다.
        2. 어보트된 트랜잭션이 쓴 데이터는 모두 무시된다.
        3. 트랜잭션 ID가 더 큰(즉 현재 트랜잭션이 시작한 후에 시작한) 트랜잭션이 쓴 데이터는 그 트랜잭션의 커밋 여부에 관계없이 모두 무시된다.
        4. 그 밖의 모든 데이터는 애플리케이션의 질의로 볼 수 있다.
      • 이 규칙은 객체 생성과 삭제 모두에 적용된다.
    • 색인과 스냅숏 격리
      • 다중 버전 데이터베이스에서 색인 동작 방식
      • 색인이 객체의 모든 버전을 가리키게 하고 색인 질의가 현재 트랙잭션에서 볼 수 없는 버전을 걸러내게 한다. 가비지 컬렉션이 어떤 트랜잭션에게도 더 이상 보이지 않는 오래된 객체 버전을 삭제할 때 대응되는 색인 항목도 삭제된다.
    • 스냅숏 격리는 유용한 격리 수준이며 특리 읽기 전용 트랜잭션에 유용하며, 오라클에서는 직렬성, 포스트그레스큐엘과 MySQL 에서는 반복 읽기라고 한다.

     

    갱신 손실 방지

    • 동시에 실행되는 쓰기 트랜잭션 사이에 발생할 수 있는 충돌로, 애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때(read-modify-write 주기) 발생할 수 있다.
    • 만약 두 트랜잭션이 이 작업을 동시에 하면 두 번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경 중 하나는 손실될 수 있다.

     

    원자적 쓰기 연산

    • 여러 데이터베이스에서 원자적 갱신 연산을 제공한다.
    • UPDATE counters SET value = value + 1 WHERE key = 'foo';
    • 대부분의 관계형 데이터베이스에서 동시성 안전(concnrrency-safe)하다.
    • 원자적 연산은 객체를 읽을 때 그 객체에 독점적인 잠금을 획득해서 구현한다. 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못하게 한다. => 커서 안정성(cursor stability)

     

    명시적 잠금

    • 데이터베이스에 내장된 원자적 연산이 필요한 기능을 제공하지 않는 경우 갱신 손실을 막기 위해 애플리케이션에서 갱신할 객체를 명시적으로 잠글 수 있다.
    • 애플리케이션이 read-modify-write 주기를 수행하고 있을 때 다른 트랜잭션이 동시에 같은 객체를 읽으려고 할 때 첫번째 주기가 완료될 때까지 기다리도록 강제된다.

     

    갱신 손실 자동 감지

    • 병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 어보트시키고 read-modify-write 주가를 재시도하도록 강제하는 방법이다.

     

    Compare-and-set

    • 값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 혀용함으로써 갱신 손실을 회피한다.
    • 현재 값이 이전에 읽은 값과 일치하지 않으면 갱신은 반영되지 않고 read-modify-write 주기를 재시도해야 한다.
    • UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old content';

     

     충돌 해소와 복제

    • 복제가 적용된 데이터베이스의 경우 여러 노드에 데이터의 복사본이 있어서 데이터가 다른 노드들에서 동시에 변경될 수 있으므로 갱신 손실을 방지하려면 추가 단계가 필요하다.
    • 쓰기가 동시에 실행될 때 한 값에 대해 여러 개의 충돌된 버전(형제,sibling)을 생성하는 것을 허용하고 사후에 애플리케이션 코드나 특별한 데이터 구조를 사용해 충돌을 해소하고 이 버전들을 병합한다.

     

    쓰기 스큐와 팬덤

    • 쓰기 스큐(write skew)
      • 트랙잭션이 동시에 실행됐을 때 발생하는 이상 현상으로, 두 트랜잭션이 두 개의 다른 객체를 갱신하므로 더티 쓰기도 갱신 손실도 아니다.
      • 두 트랜잭션이 같은 객체들을 읽어서 그 중 일부를 갱신할 때 나타날 수 있다.(다른 트랜잭션은 다른 객체를 갱신한다.)
    • 예시
      • 병원의 호출 대기 관리 애플리케이션
      • 회의실 예약 시스템
      • 다중플레이어 게임
      • 사용자명 획득
    • 쓰기 스큐 패턴
      1. SELECT 질의가 어떤 검색 조건에 부합하는 로우를 검색함으로써 어떤 요구사항을 만족하는지 확인한다.
      2. 첫 번째 질의의 결과에 따라 애플리케이션 코드는 어떻게 진행할지(해당 연산을 계속 처리할지 사용자에게 오류를 보고하고 중단할지) 결정한다.
      3. 애플리케이션이 계속 처리하기로 결정했다면 데이터베이스에 쓰고 트랜잭션을 커밋한다. => 해당 커밋 후 1단계 SELECT 질의를 재실행하면 다른 결과를 얻게 된다.
    • 팬텀(phantom)
      • 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과
      • 스냅숏 격리는 읽기 전용 질의에서는 팬텀을 회피하지만 읽기 쓰기 트랜잭션에서는 팬텀이 쓰기 스큐의 까다로운 경우를 유발할 수 있다.
    • 충돌 구체화(materializing conflict)
      • 팬텀을 데이터베이스에 존재하는 구체적인 로우 집합에 대한 잠금 충돌로 변환하는 것(ex. 회의실 예약)
      • 방법을 알아내기 어렵고 오류가 발생하기 쉽다.
      • 동시성 제어 메커니즘이 애플리케이션 데이터 모델로 새러 나오는 것이 보기 좋지 않다.

     

    직렬성

    • 격리 수준의 문제점
      • 격리 수준은 이해하기 어렵고 데이터베이스가 그 구현에 일관성이 없다.
      • 애플리케이션 코드를 보고 특정한 격리 수준에서 해당 코드를 실행하는게 안전한지 알기 어렵다.
      • 경쟁 조건을 감지하는데 도움이 되는 좋은 도구가 없다.
    • 문제를 해결하기 위해 직렬성 격리를 사용
    • 직렬성 격리
      • 보통 가장 강력한 격리 수준이다.
      • 여러 트랜잭션이 병렬로 실행되더라도 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장한다.
      • 데이터베이스가 발생할 수 있는 모든 경쟁 조건을 막아준다.
    • 직렬성 격리 기법
      • 트랜잭션을 순차적으로 실행하기
      • 2단계 잠금
      • 직렬성 스냅숏 격리 같은 낙관적 동시성 제어 기법

     

    실제적인 직렬 실행

    • 동시성 문제를 피하는 가장 간단한 방법은 동시성을 완전히 제거하는 것
    • 한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하면 된다. 그러면 트랜잭션 사이의 충돌을 감지하고 방지하는 문제를 완전히 회피할 수 있어 격리 수준은 직렬성 격리가 된다.
    • 단일 스레드 실행이 가능한 이유
      • 램 가격이 저렴해져서 많은 사용 사례에서 활성화된 데이터셋 전체를 메모리에 유지할 수 있다. 트랜잭션이 접근해야 하는 모든 데이터가 메모리에 있는 경우 데이터를 디스크에서 읽어 오기를 기다려야 할 때보다 트랜잭션이 훨씬 빨리 실행될 수 있다.
      • OLTP 트랜잭션은 보통 짧고 실행하는 읽기와 쓰기의 개수가 적다.

     

    트랜잭션을 스토어드 프로시저 안에 캡슐화하기

    • OLTP 애플리케이션은 트랜잭션 내에서 대화식으로 사용자 응답을 대기하는 것을 회피함으로써 트랜잭션을 짧게 유지한다.
    • 트랜잭션은 계속 상호작용하는 클라이언트/서버 스타일로 실행돼 왔고, 한 번에 구문 하나씩 실행하는 방식이다.
    • 상호작용식 트랜잭션 : 애플리케이션 코드와 데이터베이스 서버 사이에서 질의와 결과를 주고받는다. 이는 네트워크 통신에 많은 시간을 소비하게 된다.
    • 애플리케이션은 트랜잭션 코드 전체를 스토어드 프로시저 형태로 데이터베이스에 미리 제출한다. 트랜잭션에 필요한 데이터는 모두 메모리에 있고 스토어드 프로시저는 네트워크나 디스크 I/O 대기 없이 매우 빨리 실행된다.

     

    스토어드 프로시저의 장단점

    • 단점
      • 데이터베이스 벤거마다 제각각 스토어드 프로시저용 언어가 있다.(라이브러리 생태계가 약하다.)
      • 데이터베이스에서 실행되는 코드는 관리가 어렵다.
      • 데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감할 때가 많다. 여러 애플리케이션 서버에서 데이터베이스 인스턴스 하나를 공유하기 때문이다.
    • 스토어드 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일 스레드에서 실행하는 게 현실성 있다.
    •  장점
      • I/O 대기가 필요없다.
      • 다른 동시성 제어 메커니즘의 오버헤드를 회피하므로 단일 스레드로 상당히 좋은 처리량을 얻을 수 있다.

     

    파티셔닝

    • 읽기 전용 트랜잭션은 스냅숏 격리를 사용해 다른 곳에서 실행될 수 있지만 쓰기 처리량이 높은 애플리케이션에게는 단일 스레드 트랜잭션 처리자가 심각한 병목이 될 수 있다.
    • 단일 파티션
      • 각 트랜잭션이 단일 파티션 내에서만 데이터를 읽고 쓰도록 데이터셋을 파티셔닝하는 경우 각 파티션은 다른 파티션과 독립적으로 실행되는 자신만의 트랜잭션 처리 스레드를 가질 수 있다.
      • 각 CPU 코에어 각자의 파티션을 할당해서 트랜잭션 처리량을 CPU 코어 개수레 맞춰 선형적으로 확장할 수 있다.
    • 여러 파티션
      • 데이터베이스가 해당 트랜잭션이 접근하는 모든 파티션에 걸쳐서 코디네이션을 해야 한다.
      • 스토어드 프로시저는 전체 시스템에 걸쳐 직렬성을 보장하기 위해 모든 파티션에 걸쳐 잠금을 획득한 단계에서 실행돼야 한다.
      • 추가적인 코디네이션 오버헤드로 인해 단일 파티션 트랜잭션보다 엄청나게 느리고 처리량이 매우 낮다.

     

    직렬 실행 요약

    • 모든 트랜잭션은 작고 빨라야 한다.
    • 활성화된 데이터셋이 메모리에 적재될 수 있는 경우로 사용이 제한된다.
    • 쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 충분히 낮아야 한다.
    • 여러 파티션에 걸친 트랜잭션도 쓸 수 있지만 이것을 사용할 수 있는 정도에는 엄격한 제한이 있다.

     

    2단계 잠금(two-phase locking, 2PL)

    • 직렬성을 구현하는 데 널리 쓰이는 유일한 알고리즘
    • 쓰기를 실행하는 트랜잭션이 없는 객체는 여러 트랜잭션에서 동시에 읽을 수 있지만, 누군가 어떤 객체에 쓰러고하면 독점적인 접근이 필요하다.
      • 트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 그 객체에 쓰기를 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다.
      • 트랜잭션 A가 객체에 썼고 트랜잭션 B가 그 객체를 읽기 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다.
    • 쓰기 트랜잭션은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막고 그 역도 성립한다.
    • 스냅숏 격리의 '읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 결코 읽는 쪽을 막지 않는다'는 원칙과 차이가 존재한다.

     

    2단계 잠금 구현

    • 잠금은 공유 모드(shared mode)나 독점 모드(exclusive mode)로 사용될 수 있다.
      • 트랜잭션이 객체를 읽기 원한다면 먼저 공유 모드로 잠금을 획득해야 한다. 동시에 여러 트랜잭션이 공유 모드로 잠금을 획득하는 것은 허용되지만 만약 그 객체에 이미 독점 모드로 잠금을 획득한 트랜잭션이 있으면 해당 트랜잭션이 완료될 때까지 기다려야 한다.
      • 트랜잭션이 객체에 쓰기를 원한다면 먼저 독점 모드로 잠금을 획득해야 한다.(동시 획득 불가능)
      • 트랜잭션이 객체를 읽다가 쓰기를 실행할 때는 공유 잠금을 독점 잠금으로 업그레이드해야 한다.
      • 트랜잭션이 잠금을 획득한 후에는 트랜잭션이 종료될 때까지 잠금을 갖고 있어야 한다.(1단계는 잠금을 획득, 2단계는 모든 잠금을 해제)
    • 교착 상태가 발생할 수 있다. 데이터베이스는 교착 상태를 자동으로 감지하고 트랜잭션 중 하나를 어보트시켜서 다른 트랜잭션들이 진행할 수 있게 하고 어보트된 트랜잭션은 애플리케이션에서 재시도해야 한다.

     

    2단계 잠금 성능

    • 완화된 격리 수준보다 트랜잭션 처리량과 질의 응답 시간이 크게 나빠진다.
    • 원인
      • 잠금을 획득하고 해제하는 오버헤드
      • 동시성 감소
      • 관계형 데이터베이스에서 트랜잭션의 실행 시간을 제한하지 않기 때문에 잠금을 획득한 트랜잭션이 끝날 때까지 무한정 대기하게 되고 대기열이 생겨 여러 다른 트랜잭션들이 완료되기를 기다려야 한다.
      • 교착 상태가 자주 발생하고, 교착 상태로 인해 어보트된 트랜잭션을 재시도하면 작업을 전부 다시 해야 한다.

     

    서술 잠금(predicate lock)

    • 팬텀을 방지하기 위해 사용되며, 어떤 검색 조건에 부합하는 모든 객체를 잠금
    • 구현
      • 트랜잭션 A가 어떤 조건에 부합하는 객체를 읽기 원한다면 질의의 조건에 대한 공유 모드 서술 잠금을 획득해야 한다. 다른 트랜잭션이 그 조건에 부합하는 어떤 객체에 독점 잠금을 갖고 있으면 질의를 실행하도록 허용되기 전에 잠금이 해제하기를 기다려야 한다.
      • 어떤 객체를 삽입, 갱신, 삭제하기를 원한다면 먼저 기존 값이나 새로운 값 중에 기존의 서술 잠금에 부합하는게 있는지 확인하고, 부합하는게 있다면 종료될 때까지 대기해야 한다.
    • 서술 잠금은 데이터베이스에 아직 존재하지 않지만 미래에 추가될 수 있는 객체에도 적용할 수 있다.
    • 2단계 잠금이 서술 잠금을 포함하면 데이터베이스에서 모든 형태의 쓰기 스큐와 다른 경쟁 조건을 막을 수 있어서 격리 수준이 직렬성 격리가 된다.
    • 하지만 진행 중인 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 잠금을 확인하는 데 시간이 오래 걸린다.

     

    색인 범위 잠금(index-range locking, 다음 키 잠금(next-key locking))

    • 명확히 조건에 부합하는 객체만 잠그는 것이 아니라 근사 조건에 부합하면 잠금
    • 보통 검색 조건이 색인에 붙는 경우가 많아 색인에 잠금을 건다.
    • 다른 트랜잭션에서 해당 로우를 갱신하고 싶다면 색인의 같은 부분을 갱신해야 한다. 공유 잠금을 발견하는 경우 해제될 때까지 대기해야 한다.
    • 팬텀과 쓰기 스큐로부터 보호해주는 효과가 있다.
    • 서술 잠금보다 정밀하지는 않지만 오버헤드가 훨씬 낮다.

     

    직렬성 스냅숏 격리(serializable snapshot isolation, SSI)

    • 2단계 잠금 : 성능이 좋지 않음
    • 직렬성 구현 : 확장이 잘 되지 않음
    • 완화된 격리 수준 : 성능은 좋지만 다양한 경쟁 조건(갱신 손실, 쓰기 스큐, 팬텀 등)에 취약
    • 직렬설 스냅숏 격리는 완전한 직렬성을 제공하지만 스냅숏 격리에 비래 약간의 성능 손해만 존재한다.

     

    비관적 동시성 제어 vs 낙관적 동시성 제어

    • 비관적 동시성 제어
      • 2단계 잠금 : 뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때까지 기다리는게 낫다는 원칙을 기반 
      • 직렬 실행 : 각 트랜잭션이 실행되는 동안 전체 데이터베이스에 독점 잠금을 획득하는 것과 같다.
    • 낙관적 동시성 제어
      • 직렬성 스냅숏 격리 : 뭔가 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 모든 것이 괜찮아질 거라는 희망을 갖고 계속 진행한다.
      • 트랜잭션이 커밋되기를 원할 때 데이터베이스는 나쁜 상황이 발생했는지 확인하고, 그렇다면 어보트되고 재시도한다.
      • 단점
        • 경쟁이 심하면 어보트시켜야 할 트랜잭션의 비율이 높아지므로 성능이 떨어진다.
        • 시스템이 최대 처리량에 근접한 경우 재시도되는 트랜잭션으로 인해 성능이 저하될 수 있다.
      • 장점
        • 예비 용량이 충분하고 트랜잭션 사이의 경쟁이 심하지 않는 경우 성능이 더 좋다. 경쟁은 가환 원자적 연산을 통해 줄일 수 있다.

     

    뒤쳐진 전제에 기반한 결정

    • 트랜잭션은 어떤 전제를 기반으로 동작하는데, 해당 트랜잭션이 커밋하려고 할 때 원래 데이터가 바뀌어서 그 전제가 더 이상 참이 아닐 수 있다.
    • 직렬성 격리를 제공하려면 데이터베이스는 트랜잭션이 뒤처진 전체를 기반으로 동작하는 상황을 감지하고 그런 상황에서는 트랜잭션을 어보트시켜야 한다.
    • 질의 결과를 감지하는 방법
      • 오래된 MVCC 객체 버전을 읽었는지 감지하기(읽기 전에 커밋되지 않은 쓰기가 발생했음)
      • 과거의 읽기에 영향을 미치는 쓰기 감지하기(읽은 후에 쓰기가 실행됨)

     

    직렬성 스냅숏 격리의 성능

    • 트랜잭션이 다른 트랜잭션들이 잡고 있는 잠금을 기다리느라 차단될 필요가 없다.
    • 읽기 전용 질의는 어떤 잠금도 없이 일관된 스냅숏 위에서 실행될 수 있다.
    • 단일 CPU 코어의 처리량에 제한되지 않는다.

     

    'STUDY > 데이터 중심 애플리케이션 설계' 카테고리의 다른 글

    9장. 일관성과 합의  (0) 2025.09.23
    8장. 분산 시스템의 골칫거리  (0) 2025.08.31
    6장. 파티셔닝  (4) 2025.07.30
    4장. 부호화와 발전  (0) 2025.06.25
    3장. 저장소와 검색  (0) 2025.06.18
Designed by Tistory.