본문 바로가기

Studying/Data System Design

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

애매모호한 트랜잭션의 개념

  1. ACID
    1. A(원자성)은 어보트(abortability)가 더 어울린다.
    2. C(일관성)는 데이터베이스에서 달성할 수 있는 게 아니고 애플리케이션 속성이다. C는 약어를 만들기 위해서 사용됐고 중요하게 생각되지 않았다고 한다.
    3. I(격리성)은 성능 저하를 초래해서 아예 구현하지 않거나 완화된 것을 사용한다.
    4. D(지속성)은 신뢰성을 토대로 완벽한 지속성은 불가하다.
  2. 트랜잭션 격리: 동시성 문제를 감추기 위한 것으로 동시성이 없는 것처럼 한다. 대표적으로 직렬성 격리인데 현실적으로 비용이 크다.
  3. 커밋 후 읽기: 읽을 때 커밋된 데이터만 읽고, 쓸 때는 커밋된 데이터만 덮어쓴다. (더티 읽기, 더티 쓰기 방지)
    1. 더티 읽기: 커밋이나 어보트되지 않은 데이터를 읽는 것
    2. 로우 수준 잠금: 해당 객체에 대한 잠금을 획득해야 수정이 가능하고 완료되기까지 보유하고 있어야 한다.
      1. 더티 읽기도 비슷한 방식으로 막을 수 있지만, 쓰기를 하는 동안 모든 읽기가 막히면 성능에 악영향을 준다. 따라서 대부분은 과거의 값과 현재 쓰고 있는 값을 둘 다 가지고 있다가 커밋 시점에 따라 읽을 수 있게 한다.
  4. 스냅숏 격리와 반복 읽기
    1. 계좌 금액 이동 같은 경우 양쪽 계좌의 커밋 후 읽기에선 커밋 시간 차로 인해 잘못이 발생할 수 있음
    2. 더티 읽기 방법을 일반화한 것으로 객체마다 여러 개의 커밋된 버전을 갖게 한다. (다중 버전 동시성 제어 MVCC) 읽을 때 독립된 스냅숏을 읽게 한다.
    3. 쓸 때는 쓰기 잠금을 사용하고 읽기에는 잠금을 사용하지 않는다. 스냅숏 격리는 오래 걸리는 읽기만 하는 경우에 유용하다.
    4. 트랜잭션마다 id를 가지고 있다.
    5. 규칙
      1. 진행 중인 트랜잭션은 무시한다. (나중에 커밋돼도)
      2. 어보트된 트랜잭션이 쓴 데이터는 무시한다.
      3. 트랜잭션 ID가 더 큰 경우에는 무시한다.
  5. 갱신 손실 방지
    1. 갱신 손실: 동시에 쓸 때 일부 쓰기가 무시됨
    2. 원자적 쓰기 연산: value = value+1 같은 형식으로 표현
    3. 명시적인 잠금: 첫번째 read-modify-write 주기과 완료될 때까지 대기
      1. compare-and-set: 마지막으로 읽었는데 이전에 읽었을 때와 같으면 갱신 허용
  6. 쓰기 스큐와 팬텀
    1. 쓰기 스큐: 두 트랜잭션의 두 개의 다른 객체를 갱신하는데 동시에 발생하여 경쟁 조건 발생 (하나씩 했으면 한 개만 성공인데, 동시에 하여 두 개가 성공돼서 이상 동작), 갱신 손실의 일반화로 생각할 수 있음
    2. 팬텀: 어떤 트랜잭션이 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과를 팬텀이라 한다.
      1. 스냅숏 격리는 읽기 전용 질의에서는 팬텀을 회피하지만, 읽기 쓰기 트랜잭션에서는 팬텀이 쓰기 스큐를 유발할 수 있다. (조건이 되는 row를 반환하지 않을 때)
    3. 충돌 구체화(materializing conflict): 인위적으로 데이터베이스 잠금 객체를 추가 (시간표를 블록으로 만듦), 방법을 알아내기 어렵고 오류가 발생하기 쉬워 최후의 수단
  7. 직렬성 격리: 여러 트랜잭션을 동시성 없이 하나씩 실행하게 한다.
    1. 실제적인 직렬 실행: 오랜 기간 동안 성능을 이유로 사용하지 않았다가 메모리가 늘어나고, OLTP에선 짧고 읽기와 쓰기가 적어서 가능하다고 최근에 결론을 내림 → 어쨌든 cpu 코어 하나의 성능으로 제한됨
      1. 상호작용식 트랜잭션에선 대기 시간이 필요하므로 단일 스레드에서 성능에 안 좋은 영향이 있어 저장 프로시저 형태로 데이터베이스에 미리 제출한다.
        1. 저장 프로시저는 db별로 각자의 언어가 있고 코드 관리 및 성능 문제가 있었으나 최근에는 상용 애플리케이션 코드로 사용할 수 있게 변화 중
      2. 파티셔닝을 활용해 파티션 별로 트랜잭션을 처리할 수 있으나, 여러 파티션에 접근 하는 경우가 있을 수 있고 이럴 경우 성능에 악영향을 미침
    2. 2단계 잠금: 쓰기를 실행하는 트랜잭션이 없는 개체는 여러 트랜잭션에서 동시에 읽을 수 있으나 누군가 쓰려고 하면 독점적인 접근이 필요하다. (읽을 수도 없음)
      1. 공유모드 잠금: 읽을 때 사용하는 잠금
      2. 독점모드 잠금: 쓸 때 사용하는 잠금
      3. 교착 상태가 발생 할 수 있어 자동으로 감지하여 어보트시켜야 한다.
      4. 동시성이 줄어들어 성능이 안 좋아 쓰지 않기 시작함
      • 서술 잠금(predicate lock): 특정 객체가 아니고 검색 조건에 부합하는 모든 객체를 잠그는 방식으로 팬텀을 막기 위해 필요
        1. 조건에 부합하는 잠금을 확인하는 데 시간이 오래 걸려서 서술 잠금은 근사한 색인 범위 잠금(index-range locking)을 사용한다.
          1. 특정 시간대의 모든 회의실을 잠그거나 모든 시간대의 특정 회의실을 잠금
    3. 직렬성 스냅숏 격리(SSI) 같은 낙관적 동시성 제어(optimistic concurrency control)
      1. 2단계 잠금은 비관적 동시제어로 잘못된 가능성이 있으면 기다리는 게 낫다는 방식
      2. 직렬성 스냅숏 격리는 낙관적 동시제어로 트랜잭션이 커밋할 때 데이터베이스의 격리가 위반됐는 지 확인하고 그렇다면 어보트한다. (일단 실행하고 마지막에 확인하는 방식)
        1. 경쟁이 심할 때는 어보트가 많아져 성능이 떨어지나 경쟁이 적을 때는 성능이 좋다.
        2. SSI는 일관된 스냅숏을 보게 하여 스냅숏 격리 위에서 직렬성 충돌을 감지하여 어보트 시키는 알고리즘을 추가함
      3. 스냅숏 격리에서는 쓰기 스큐의 문제로 질의 결과가 뒤쳐졌을 수 있다는 것을 확인해야 한다.
        1. 오래된(stale) MVCC 읽기 감지 (읽기 전에 커밋되지 않은 쓰기가 발생했음)
          1. 트랜잭션이 커밋을 하려 할 때 무시된 쓰기 중에 커밋된 게 있는 지 확인
          2. 커밋이 기준인 이유는 쓰기 여부를 확인하기 힘들고 불필요한 어보트를 피하기 위함
        2. 과거의 읽기에 영향을 미치는 쓰기 감지하기 (읽은 후에 쓰기가 실행됨)
          1. 임시로 각 트랜잭션들이 특정 데이터를 읽었다는 정보를 저장한다.
          2. 트랜잭션이 데이터를 쓸 때 영향받는 데이터를 최근에 읽은 트랜잭션이 있는 지 확인한다. 트랜잭션끼리 서로 읽은 데이터가 뒤처졌다고 알려준다.
          3. 먼저 커밋을 시도한 것은 성공하고 이후에 커밋을 하면 어보트한다.