Skip to main content

7. 트랜잭션

현실에서 데이터 시스템은 여러 가지 문제가 생길 수 있습니다.

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

시스템이 신뢰성을 지니려면 이러한 결함을 처리해서 전체 시스템의 치명적인 장애로 이어지는 것을 막아야 합니다.

  • 트랜잭션은 이런 문제를 단순화하는 메커니즘으로 채택되었습니다.
  • 트랜잭션은 애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법입니다.
    • 개념적으로 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행됩니다.
    • 트랜잭션은 전체가 성공(커밋) 하거나 실패(abort, 롤백) 합니다
    • 트랜잭션을 사용하면 안전하게 재시도할 수 있기 때문에 오류 처리를 하기 훨씬 단순해집니다.
  • 트랜잭션은 데이터베이스에 접속하는 애플리케이션에서 프로그래밍 모델을 단순화하려는 목적으로 만들어졌습니다.
    • 트랜잭션을 통해 잠재적인 오류 시나리오와 동시성 문제를 무시할 수 있습니다. (안전성 보장, safety guarantee)
  • 모든 애플리케이션에서 트랜잭션이 필요하지 않으며 때로는 트랜잭션적인 보장을 완화하건아 아예 쓰지않는 것이 이득입니다.

데이터베이스에서 문제가 생길 수 있는 여러 예를 조사하고 이를 사용하는 알고리즘을 살펴봅니다. 동시성 제어 분야를 깊게 다루며 발생할 수 있는 다양한 종류의 경쟁조건과 데이터베이스에서 커밋 후 읽기(read committed), 스냅숏 격리(snapshot isolation), 직렬성(serializability) 같은 격리 수준을 어떻게 같은 격리 수준을 구현하는지에 대해 설명합니다.


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

현대의 거의 모든 관계형 데이터베이스와 일부 비관계형 데이터베이스는 트랜잭션을 지원합니다.

  • 비관계형(NoSQL) 데이터베이스가 주류로 바뀌며 트랜잭션을 약한 보장을 의미하는 단어로 트랜잭션 의미가 재정의 되었습니다.
  • 트랜잭션은 이점과 한계가 있습니다. 이에 따라 잘 조절해야합니다.

ACID의 의미#

트랜잭션이 제공하는 안전성 보장은 크게 아래로 나눠집니다.

  • 원자성(Atomicity)
  • 일관성(Consistency)
  • 격리성(Isolation)
  • 지속성(Durability)

그러나 현실에서는 데이터베이스마다 ACID 구현이 제각각이며 격리성은 의미 주변에 모호함이 많습니다. (ACID 표준을 따르지 않는 시스템은 때로 BASE라 불립니다. 가용성을 제공하고(Basically Available), 유연한 상태를 가지며(Soft state), 최종적 일관성(Eventual consistency) 의 의미를 가집니다.)

원자성#

  • 일반적으로 원자적이란 더 작은 부분으로 쪼갤 수 없는 무엇을 가르킵니다.
  • ACID의 원자성은 클라이언트가 쓰기 작업을 몇 개 실행할 때, 여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶여 있는데 결함 때문에 완료(커밋)될 수 없으면 어보트되고 데이터베이스는 이 트랜잭션에서 지금까지 실횅한 쓰기를 무시하거나 취소해야합니다.
  • 오류가 생겼을 때 트랜잭션을 어보트하고 해당 트랜잭션에서 기록한 모든 내용을 취소하는 능력은 ACID의 원자성의 결정적인 특징입니다.

일관성#

  • 일관성은 괴장히 여러 의미로 쓰입니다. (복제 일관성, 최종적 일관성, 일관성 해싱, CAP의 일관성 등등)
  • ACID의 일관성은 데이터 베이스가 좋은 상태(항상 진실이어야하는 데이터에 관한 어떤 선언, 불변식(invariant)가 있다는 것) 에 있어야 한다는 애플리케이션에 특화된 개념입니다.
  • 일관성의 아이디어는 애플리케이션의 불변식 개념에 의존하고 일관성을 유지하도록 트랜잭션을 올바르게 정의하는 것은 애플리케이션의 책임입니다.
  • 원자성, 격리성, 지속성은 데이터베이스의 속성인 반면 일관성은 애플리케이션의 속성입니다.

격리성#

  • 대부분 동시에 여러 클라이언트에서 데이터베이스에 접근하고, 클라이언트 들이 동일한 데이터베이스 레코드에 접근하면 동시성 문제에 맞닥뜨리게 됩니다.
  • ACID의 격리성은 동시에 실행되는 트랜잭션은 서로 격리되는 것을 의미합니다.

지속성#

  • 데이터베이스 시스템의 목적은 데이터를 잃어버릴 염려가 없는 안전한 저장소를 제공하는 것입니다.
  • 지속성(durability)는 트랜잭션이 성공적으로 커밋되었다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 보장입니다.

복제와 지속성

  • 고가ㅓ에는 지속성이 아카이브 테이프에 기록되는 것을 의미하였고, 그 이후는 디스크나 SSD에 기록하는 것, 현재는 복제를 의미합니다.
  • 이중에서 왑녁한 것은 없으며, 여러 위험을 줄이는 기법일 뿐이며 같이 쓸 수 있으면 그렇게 해야합니다.

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

요약하면 ACID에서 원자성과 격리성은 클라이언트가 한 트랜잭션 내에서 여러 번의 쓰기를 하면 데이터베이스가 어떻게 해야하는지를 서술합니다.

  • 원자성 : 데이터베이스는 전부 반영되거나 아무것도 반영되지 않음을 보장합니다.
  • 격리성 : 동시에 실행되는 트랜잭션들은 서로를 방해하지 말아야 합니다.

다중 객체 트랜잭션은 흔히 데이터의 여러 조각이 동기화된 상태로 유지되어야 할 때 필요합니다.

  • 다중 객체 트랜잭션은 어떤 읽기 연산과 쓰기 연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 있어야 합니다.
  • 비관계형 데이터베이스는 이런식으로 연산을 묶는 경우가 없어서, 다중 객체 API가 있더라도 반드시 트랜잭션 시맨틱을 의미하지는 않습니다.

단일 객체 쓰기#

  • 원자성과 격리성은 단일 객체를 변경하는 경우에도 적용됩니다.
  • 단일 객체 연산은 여러 클라이언트에서 동시에 같은 객체에 쓰려고 할 때 갱신 손실(lost update)를 방지하므로 유용합니다.

다중 객체 트랜잭션의 필요성#

  • 많은 분산 데이터스토어는 다중 객체 트랜잭션 지원을 포기했습니다.
    • 다중 객체 트랜잭션은 여러 파티션에 걸쳐서 구현하기도 어렵고 매우 높은 가용성과 성능이 필요한 곳에서는 방해가 되는 시나리오가 있습니다.
  • 단일 객체, 갱신, 삭제만으로도 충분한 사용 사례가 있습니다. (굳이 다중 객체 트랜잭션이 필요 없는 경우)

오류와 어보트 처리#

  • 트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도할 수 있습니다.
  • 다만 모든 시스템이 이러한 철학을 따르지 않으며, 리더 없는 복제를 사용하는 데이터 스토어는 "최선을 다하는(best effort)" 원칙을 기반으로 더 많은 일을 합니다.
    • 이 경우, 오류 복구는 애플리케이션에게 책임이 있습니다.
  • 어보트된 트랜잭션을 재시도하는 것은 간단하고 효과적인 오류 처리 메커니즘이지만 완벽하지않습니다.
    • 트랜잭션이 실제로 성공했으나 네트워크 이슈가 발생한 경우
    • 오류가 과부하 때문에 발생한 경우
    • 일시적인 오류만 재시도할 가치가 있으며 영구적인 오류는 재시도해도 소용이 없습니다.
    • 트랜잭션이 데이터베이스 외부에도 부수 효과가 있다면 트랜잭션이 어보트될 때도 부수 효과가 실행될 수 있습니다.
    • 클라이언트 프로세스가 재시도 중에 죽어버리면 클라이언트에서 데이터베이스에 쓰려고 했던 데이터가 모두 손실이 됩니다.

완화된 격리 수준#

  • 두 트랜잭션이 동일한 데이터에 접근하지 않으면 안전하게 병렬 실행할 수 있습니다.
  • 동시성 버그는 타이밍에 운이 안좋을때만 발생하기 때문에 테스트로 발견하기 어렵습니다.
  • 이러한 이유로 데이터베이스는 오랫동안 데이터 격리를 제공했습니다.
    • 다만 비용이 매우 큰 문제가 있습니다
  • 많은 데이터베이스는 완화된 트랜잭션 격리를 제공합니다.
    • 이는 미묘한 버그를 만드는 문제가 있습니다.
  • 일반적으로 도구를 의존하기 보다 동시성 문제를 잘 이해하고 방지하는 방법을 배울 필요가 있습니다.

Q. 우리 회사에서는 뭐쓰지

Q. 옛날에 관련해서 본 4단계 계층 구조가 있는데 확인해보기

커밋 후 읽기#

가장 기본적인 수준의 트랜잭션은 커밋 후 읽기(read committed) 입니다. 이 수준에서는 두 개를 보장해줍니다.

  • 데이터베이스에서 읽을 때, 커밋된 데이터만 읽게 됩니다. (더티 읽기가 없음)
  • 데이터베이스에서 쓸 때 커밋된 데이터만 덮어쓰게 됩니다. (더티 쓰기가 없음)

더티 읽기 방지#

  • 더티 읽기(dirty read) : 트랜재션이 데이터베이스에 데이터를 썼지만 아직 커밋되지 않았거나 어보트 되지 않은 데이터
  • 이를 막으면 다음의 장점을 가집니다
    • 부분적 갱신 데이터는 사용자에게 혼란스러움을 제공할 수 있습니다.
    • 트랜잭션이 어보트시 롤백을 해야하는데 혼란을 줄 수 있습니다.

더티 쓰기 방지#

  • 더티 쓰기(dirty write) : 먼저 쓴 내용이 아직 커밋에서 쓴 것이고, 나중에 실행된 쓰기 작업이 커밋되지않은 값을 덮어 쓴 경우
  • 이를 막으면 다음의 장점을 가집니다
    • 트렌잭션이 여러 객체를 갱신하면 나쁜 결과를 유발하는 문제가 있습니다.
    • 경쟁 조건을 막을 수 있습니다.

커밋 후 읽기 구현#

  • 현재 자주 쓰이는 격리 수준
  • 오라클 11g, PostgreSQL, SQL Server 2012 등에서 기본 설정입니다
  • 흔한 방법으로 데이터베이스는 로우에 대한 락을 가짐으로 더티 쓰기를 방지합니다.
  • 읽기 쓰기 잠금은 현실에서 잘 동작하지 않습니다.
    • 따라서 과거와 새로운 값을 모두 기억하고 커밋 전까지는 다른 트랜잭션들이 과거의 값을 가집니다.

스냅숏 격리와 반복 읽기#

  • 커밋 후 읽기 격리를 피상적으로 보면 트랜잭션이 해야하는 모든 일을 해주는 것으로 생각하는 것도 무리가 아닙니다
  • 위의 격리 수준을 사용해도 깨지는 경우가 존재합니다.

읽기 쓰큐, 깨진 데이터를 본 경우

  • 위의 이상 현상을 비반복 읽기(nonrepeatedable read)읽기 스큐(read skew) 라고 합니다.
  • 이러한 일시적인 문제를 감내할 수 없는 경우가 있습니다.
    • 백업 : 데이터베이스 전체의 복사본을 만들어야하는 경우에 비 일관성이 발생하면 이를 영속적으로 저장하게 굅니다.
    • 분석 질의와 무결성 확인 : 데이터베이스의 큰 부분을 질의하는 경우
  • 스냅숏 격리는 이런 문제의 흔한 해결책입니다. 각 트랜잭션은 데이터베이스의 일관된 스냅숏으로부터 읽습니다.
  • 스냅숏 격리는 백업이나 분석처럼 실행하는게 오래 걸리며 읽기만 실행하는 질의에 요긴합니다.
  • 스냅숏 격리는 PostgreSQL, InnoDB 저장소 엔진, 마이SQL, 오라클, SQL Server 등에서 지원됩니다.

스냅숏 격리 구현#

  • 스냅숏 격리 구현은 커밋 후 읽기 격리처럼 전형적으로 더티 쓰기를 방지하기 위해 쓰기 잠금을 사용합니다.
  • 성능 관점에서 스냅숏 격리의 핵심 원리는 읽는 쪽에서 쓰는 쪽을 절대 차단하지 않고 쓰는 쪽에서 읽는 쪽을 결코 차단하지 않는다는 것입니다.
  • 진행 중인 여러 트랜잭션에서 서로 다른 시점의 데이터베이스 상태를 봐야하기 때문에 데이터베이스는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 합니다. 이 기법을 다중 버전 동시성 제어(multi-version concurrency control, MVCC)라고 합니다.
  • 데이터베이스가 스냅숏 격리가 아니라 커밋 후 읽기 격리만 제공할 필요가 있다면 객체마다 버전 두개씩 쥬히면 충분합니다.

[이미지]

일관된 스냅숏을 보는 가시성 규칙#

  • 트랜잭션은 데이터베이스에서 객체를 읽을 때 트랜잭션 ID를 사용해 어떤 것을 볼 수 있고 어떤 것을 볼 수 없는지 결정합니다.
  • 두 조건이 참이면 객체를 볼 수 있습니다.
    • 읽기를 실행하는 트랜잭션이 시작한 시점에 읽기 대상 객체를 생성한 트랜잭션이 이미 커밋된 상태였다
    • 읽기 대상 객체가 삭제된 것으로 표시되지 않았다 또는 삭제된 것으로 표시되었지만 읽기를 실행한 트랜잭션이 시작한 시점에 삭제 요청 트랜잭션이 아직 커밋되지 않았다.
  • 데이터 베이스는 갱신할 때 값을 교체하지 않고 값이 바뀔 때마다 새 버전을 생성함으로써 작은 오버헤드만 유발하면서 일관된 스냅숏을 제공할 수 있습니다.

색인과 스냅숏 격리#

다중 버전 데이터베이스에서 색인은 아래처럼 동작합니다.

  • 간단한 방법으로 단순하게 색인이 객체의 모든 버전을 가리키게 하고 색인 질의가 현재 트랜잭션에서 볼 수 없는 버전을 걸러내게 하는 것
  • 현실에서는 여러 구현 세부 사항에 따라 성능이 결정되므로 PostgreSQL은 동일한 객체의 다른 버전들이 같은 페이지에 저장될 수 있다면 색인 갱신을 회피하는 최적화를 합니다.
  • CouchDB, LMDB 에서는 B 트리 변종(쓸때 복사되는, append-only/copy-on-write)을 사용합니다.
  • 추가 전용 B 트리도 컴팩션과 가비지 컬렉션을 실행하는 백그라운드 프로세스가 필요합니다.

반복 읽기와 혼란스러운 이름#

  • 스냅숏 격리는 유용한 격리 수준이며 특히 읽기 전용 트랜잭션에 유용합니다.
    • 오라클에서는 직렬성, PostgreSQL과 MySQL에서는 반복 읽기(repeatable read) 라고 합니다.
    • 이름이 혼란스러운 이유는 SQL 표준에 스냅숏 격리 개념이 없어서 그렇습니다.
  • SQL 표준의 격리 수준 정의에는 결함이 있습니다. 모호하고 부정확하며 표준이 그래야하는 것만큼 구현이 독립적이지 않습니다.

갱신 손실 방지#

  • 갱신 손실(lost update) 문제는 애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때, 발생할 수 있습니다.
  • 두 트랜잭션이 이 작업을 동시에 하면 두 번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경 중 하나는 손실될 수 있습니다. (나중에 쓴게 먼저 쓴거를 덮어씁니다.)
  • 아래의 경우에서 발생할 수 있습니다.
    • 카운터를 증가시키거나 계좌 잔고를 갱신하는 경우
    • 복잡한 값을 지역적으로 변경합니다.
    • 위키에서 두 명의 사용자가 동시에 같은 페이지를 편집한다.

원자적 쓰기 연산#

  • 여러 데이터베이스에서 원자적 갱신 연산을 제공합니다.
  • 원자적 연산은 보통 객체를 읽을 때 그 객체에 독점적인(exclusive) 잠금을 획득해서 구현합니다. 그래서 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못하게 합니다. (커서 안전성, cursor stability)

명시적인 잠금#

  • 데이터베이스에서 내장된 원자적 연산이 필요한 기능을 제공하지 않을 때 갱신 손실을 막는 방법은 애플리케이션에서 갱신할 객체를 명시적으로 잠그는 것입니다.
  • 올바르게 동작하려면 애플리케이션 로직에 대해 신중하게 생각해야 합니다.
BEGIN TRANSACTION
SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222
FOR UPDATE;
-- 이동이 유효한지 확인후, 반환된 것의 위치 갱신
UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;

갱신 손실 자동 감지#

  • 원자적 연산과 잠금은 read-modify-write 주기가 순차적으로 실행되도록 강제함으로써 갱신 손실을 방지하는 방법입니다.
  • 이 방법의 이점은 데이터베이스가 이 확인을 스냅숏 격리와 결합해 효율적으로 수행활 수 있다는 것입니다.
  • 갱신 손실 감지는 애플리케이션 코드에서 어떤 특별한 데이터베이스 기능도 쓸 필요가 없게 도와주므로 매우 좋은 기능입니다.

Compare-and-set#

  • 연산의 목적은 값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용함으로써 갱신 손실을 회피하는 것입니다.
  • 현재 값이 이전에 읽은 값과 일치하지 않으면 갱신은 반영되지 않고 read-modify-write 주기를 재시도 해야합니다.
-- 데이터베이스 구현에 따라 안전할 수도 안전하지 않을 수도 있습니다.
UPDATE wiki_pages SET content = 'new content'
WHERE id = 1234 AND content = 'old content';

충돌 해소와 복제#

  • 잠금과 compare-and-set 연산은 데이터의 최신 복사본이 하나만 있다고 가정합니다. 그러나 다중 리더나 리더 없는 복제는 데이터의 최신 복사본이 하나만 있다고 보장할 수 없습니다.
  • 복제가 적용된 데이터베이스에서 흔히 쓰는 방법은 쓰기가 동시에 실행될 때 한 값에 대해 여러 개의 충돌된 버전(형제, sibling) 을 생성하는 것을 허용하고 사후에 애플리케이션 코드나 특별한 데이터 구조를 사용해 충돌을 해소하고 이 버전들을 병합하는 것입니다.
  • 원자적 연산은 복제 상황에서도 잘 동작합니다.
  • 최종 쓰기 승리(last write wins, LWW) 충돌 해소 방법은 갱신 손실이 발생하기 쉽습니다. 많은 복제 데이터베이스는 LWW가 기본 설정입니다.

쓰기 스큐와 팬텀#

  • 앞에서는 다른 트랜잭션들이 동시에 같은 객체에 쓰려고 할 때 발생할 수 있는 두 가지 경쟁 조건인 더티 쓰기갱신 손실을 보았습니다.
  • 예를 위하 두 사람이 호출 대기를 하는데 동시에 호출 대기를 끄는 경우, 둘다 빠지는 문제가 발생합니다.

쓰기 스큐를 특징짓기#

  • 위의 이상 현상을 쓰기 스큐(write skew) 라고 합니다.
  • 두 트랜잭션이 두 개의 다른 객체를 갱신하므로 더티 쓰기도 갱신 손실도 아닙니다.
  • 쓰기 스큐를 갱신 손실 문제가 일반화된 것으로 생각할 수도 있고 쓰기 스큐는 두 트랜잭션이 같은 객체들을 읽어서 그중 일부를 갱신할 때 나타낼 수 있습니다.
-- 이를 잠구는 방법은 다음과 같습니다.
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;

추가적인 쓰기 스큐의 예#

  • 회의실 예약 시스템
  • 다중플레이어 게임
  • 사용자명 획득
  • 이중 사용(double-spending) 방지

쓰기 스큐를 유발하는 팬텀#

  • 아래의 비슷한 패턴을 따릅니다.
    • SELECT 질의가 어떤 검색 조건에 부합하는 로우를 검색함으로써 어떤 요구사항을 만족하는지 확인합니다.
    • 첫 번째 질의의 결과에 따라 애플리케이션 코드는 어떻게 진행할지 결정합니다.
    • 애플리케이션이 계속 처리하기로 결정했다면 데이터베이스에 쓰고 트랜잭션을 커밋합니다.
  • 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과를 팬텀(phantom) 이라고 합니다.

충돌 구체화#

  • 팬텀의 문제가 잠글 수 있는 객체가 없다는 것이라면 인위적으로 데이터베이스에 잠금 객체를 추가합니다.
  • 위의 예시에서 예약하는 트랜잭션은 테이블에서 원하는 회의실과 시간 범위에 해당하는 로우를 잠금한 후 그후에 예약이 있는지 확인하고 새 예약을 삽입합니다.
  • 이러한 방법을 충돌 구체화(materializing conflict) 라고 합니다. 팬텀을 데이터베이스에 존재하는 구체적인 로우 집합에 대한 잠금 충돌로 변환하기 때문입니다.

직렬성#

위의 내용에서 아래의 문제점도 있습니다.

  • 격리 수준은 이해하기 어렵고 데이터베이스마다 그 구현에 대한 일관성이 없습니다.
  • 애플리케이션 코드를 보고 특정한 격리 수준에서 해당 코드를 실행하는 게 안전한지 알기 어렵습니다.
  • 경쟁 조건을 감지하는데 도움이 되는 도구가 없습니다.

이때 직렬성 격리가 좋습니다. 직렬성 격리는 보통 가장 강력한 격리 수준으로 여겨지며, 여러 트랜잭션이 병렬로 실행되더라도 최종 결과는 동시성 없이 한번에 하나씩 직렬로 실행될 때와 같도록 보장합니다. 즉, 데이터베이스가 발생할 수있는 모든 경쟁 조건을 막아줍니다.

직렬성을 제공하는 데이터베이스는 대부분 세 가지 기법 중 하나를 사용합니다.

  • 트랜잭션 순차적 실행
  • 2단계 잠금
  • 직렬성 스냅숏 격리 같은 낙관적 동시성 제어

실제적인 직렬 실행#

  • 동시성 문제를 피하는 가장 간단한 방법은 동시성을 제거하는 것
  • 최근에 들어서야 실현 가능하다고 결론지었으며 이가 가능하다고 생각된 이유는 두가지 발전입니다.
    • 램 가격의 하락으로 인해 데이터셋 전체를 메모리에 유지할 수 있게 되었습니다.
    • 데이터베이스 설계자들이 OLTP 트랜잭션이 보통 짧고 실행하는 읽기와 쓰기의 개수가 적다는 것을 깨달았습니다.
  • 이들의 처리량은 CPU 코어 하나의 처리량으로 제한됨으로 단일 스레드를 최대한 활용하려면 트랜잭션이 전통적인 형태와 다르게 구성해야 합니다.

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

  • 트랜잭션을 길게 유지하면 사용자 응답을 대기하는 시간이 길어지므로 이를 회피하기 위해 트랜잭션을 짧게 유지합니다.
  • 상호작용식 트랜잭션(질의실행 -> 결과 읽기 -> 다른 질의 실행) 은 애플리케이션과 데이터베이스 사이의 네트워크 통신에 많은 시간을 소비합니다.
  • 따라서 데이터베이스에서 쓸만한 성능을 얻으려면 여러 트랜잭션을 동시에 처리할 필요가 있습니다.
  • 단일 스레드에서 트랜잭션을 순차적으로 처리하는 시스템은 상호작용하는 다중 구문 트랜잭션을 처리하지 않고, 트랜잭션 코드 전체를 스토어드 프로시저 형태로 데이터베이스에 미리 제출합니다.

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

스토어 프로시저의 단점

  • 데이터베이스 벤더마다 제각각의 스토어드 프로시저용 언어가 있으며 언어들은 범용 프로그래밍 언어의 발전을 잡지못해 노후화되어 보입니다.
  • 데이터베이스에서 실행되는 코드는 관리하기가 어렵습니다.
  • 데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감할 때가 많습니다.

스토어 프로시저의 장점

  • 현대의 스토어 프로시저는 기존의 범용 프로그래밍 언어를 제공합니다.
  • 스토어드 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일 스레드에서 실행하는 게 현실성이 있습니다. I/O 대기가 없고 동시성 제어 메커니즘의 오버헤드를 회피함으로 좋은 처리량을 얻을 수 있습니다.

파티셔닝#

  • 위의 내용처럼 모든 트랜잭션을 순차적으로 실행한다면 동시성 제어는 훨씬 간단해지지만 데이터베이스의 트랜잭션 처리량이 단일 장비에 있는 단일 CPU 코어의 속도로 제한됩니다.
  • 여러 CPU와 여러 노드로 확장하기 위해 데이터를 파티셔닝할 수도 있습니다.
  • 여러 파티션에 접근해야 하는 트랜잭션이 있다면 데이터베이스가 해당 트랜잭션이 접근하는 모든 파티션에 걸쳐서 코디네이션 해야합니다.
  • 여러 파티션에 걸친 트랜잭션은 추가적인 코디네이션 오버헤드가 있으므로 단일 파티션 트랜잭션보다 엄청나게 느립니다.
  • 트랜잭션이 단일 파티션에서 실행될 수 있는지 여부는 애플리케이션에서 사용되는 데이터 구조에 매우 크게 의존합니다.

직렬 실행 요약#

트랜잭션 직렬 실행은 몇가지 제약 사항 안에서 직렬성 격리를 획득하는 실용적인 방법이 됬습니다.

  • 모든 트랜잭션은 작고 빨라야 합니다.
  • 활성화된 데이터벳이 메모리에 적재될 수 있는 경우로 사용이 제한됩니다.
  • 쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 충분히 낮아야 합니다. 그렇지 않으면 여러 파티션에 걸친 코디네이션이 필요하지 않도록 트랜잭션을 파티셔닝 해야합니다.
  • 여러 파티션에 걸친 트랜잭션도 쓸 수 있지만 이것을 사용할 수 있는 정도에는 엄격한 제한이 있습니다.

Q. 어떤 데이터를 DB에 업데이트 해야하는데 업데이트 하나 SP랑 애플리케이션에서 나눠서 10개의 SP(부분적으로 업데이트 범위가 다른)를 쓰는 경우 어떤게 더 빠를까요?

2단계 잠금(2PL)#

  • 2단계 잠금(two-phase locking, 2PL)은 더티 쓰기의 잠금과 비슷하지만 훨씬 더 강합니다. 쓰기를 실행하는 트랜잭션이 없는 여러 트랜잭션에서 동시에 읽을 수 있으나 쓰기를 하려면 독점적인 접근이 필요합니다.
  • 2PL에서 쓰기 트랜잭션은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막고 그 역도 성립합니다.
  • 스냅숏 격리는 읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 결코 읽는 쪽을 막지 않는다는 원칙이 있으나, 2PL은 직렬성을 제공하므로 앞에서 설명한 갱신 손실과 쓰기 스큐를 포함한 모든 경쟁 조건으로부터 보호해줍니다.

2단계 잠금 구현#

  • 읽는 쪽과 쓰는 쪽을 막는 것은 데이터베이스의 각 객체에 잠금을 사용해 구현합니다.
  • 잠금은 공유 모드(shared mode)독점 모드(exclusive mode) 로 사용될 수 있습니다.
  • 잠금은 다음과 같이 사용됩니다.
    • 트랜잭션이 객체 읽기를 원한다면 먼저 공유모드로 잠금을 획득합니다.
    • 트랜잭션이 객체에 쓰기를 원한다면 먼저 독점 모드로 잠금을 획득해야합니다.
    • 트랜잭션이 객체를 읽다가 쓰기를 실행할 때는 공유 잠금을 독점 잠금으로 업그레이드해야 합니다.
    • 트랜잭션이 잠금을 획득한 후에는 트랜잭션이 종료될 때까지 잠금을 갖고 있어야 합니다.
  • 잠금이 아주 많이 사용됨으로 트랜잭션 A는 트랜잭션 B가 잠금을 해제하기를 기다리느라 멈춰있고, 트랜잭션 B도 트랜잭션 A가 잠금을 해제하기를 기다리느라 멈춰 있는 상황이 매우 쉽게 발생하는데 이런 상황을 교착 상태라고 합니다.
    • 데이터베이스는 트랜잭션 사이의 교착 상태를 자동으로 감지하고 트랜잭션 중 하나를 어보트 시켜서 다른 트랜잭션들이 진행할 수 있게 합니다.

2단계 잠금의 성능#

  • 2단계 잠금의 큰 약점은 성능입니다.
  • 2단계 잠금을 사용하면 완화된 격리 수준을 쓸 때보다 트랜잭션 처리량과 질의 응답 시간이 크게 나빠집니다.
  • 부분적으로는 잠금을 획득하고 해제하는 오버헤드 때문이지만 더 중요한 원인은 동시성이 줄어드는 것입니다.
  • 전통적인 관계형 데이터베이스들은 트랜잭션의 실행 시간을 제한하지 않습니다. 상호작용시 애플리케이션에서 사용자의 입력을 기다리도록 설되었습니다. 그렇기 때문에 경쟁이 있다면 매우 느릴 수 있습니다.
  • 2PL 직렬성 격리에서는 교착 상태가 훨씬 더 자주 발생합니다.

서술 잠금#

  • 직렬성 격리를 쓰는 데이터베이스는 팬텀을 막야아 합니다.
  • 개념상으로서는 서술 잠금(predicate lock)이 필요합니다.
    • 서술 잠음은 앞의 공유/독점 잠금과 비슷하게 동작하지만 특정 객체에 속하지 않고 아래와 같은 여러 검색 조건에 부합하는 모든 객체에 속합니다.
SELECT * FROM bookings
WHERE room_id = 213 AND
end_time > '2018-01-01 12:00' AND
start_time < '2018-01-01 13:00';
  • 서술 잠금이 접근을 제한하는 방법은 다음과 같습니다.
    • 트랜잭션 A가 위 SELECT 질의처럼 어떤 조건에 부합하는 객체를 읽기 원한다면 질의의 조건에 대한 공유 모드 서술 잠금을 획득해야 합니다.
    • 트랜잭션 A가 어떤 객체를 삽입, 갱신, 삭제하기를 원한다면 먼저 기존 값이나 새로운 값 중에 기존의 서술 잠금에 부합하는게 있는지 확인해야 합니다.

색인 범위 잠금#

  • 서술 잠금은 잘 동작하지 않습니다. (잠금 확인 시간이 오래걸리므로)
  • 실제로는 색인 범위 잠금(index-range locking, 다음 키 잠금(next-key locking)) 을 구현합니다.
  • 어떤 방법을 쓰던지 간략화한 검색 조건이 색인 중 하나에 붙습니다.
  • 이 방법을 쓰면 팬텀과 쓰기 스큐로부터 보호해주는 효과를 낳습니다.
    • 색인 범위 잠금은 서술 잠금보다 정밀하지는 않지만 오버헤드가 훨씬 더 낮으므로 좋은 타협안이 됩니다.
  • 범위 잠금을 잡을 수 있는 적합한 색인이 없다면 데이터베이스는 테이블 전체에 공유 잠금을 잡는 것으로 대체할 수 있습니다. (성능적으로는 좋지 않으나 안전한 대비책입니다.)

직렬성 스냅숏 격리(SSI)#

  • 직렬성 스냅숏 격리(serializable snapshot isolation, SSI)는 완전한 직렬성을 제공하고 스냅숏 격리에 비해 약간의 성능 손해만 있습니다. (최근에 등장)
  • 오늘날 SSI는 단일 노드 데이터베이스와 분산 데이터베이스 모두에서 사용됩니다.

비관적 동시성 제어 대 낙관적 동시성 제어#

  • 2단계 잠금은 이른바 비관적 동시성 제어 메커니즘 입니다.
    • 뭔가 잘못될 가능성이 있으면 뭔가 하기 전에 상황이 다시 안전해질 때까지 기다리는 게 낫다는 원칙을 기반으로 합니다.
    • 다중 스레드 프로그래밍에서 자료구조 보호를 위해 사용되는 상호 배제(mutual exclusion) 와 비슷합니다.
  • 직렬 실행은 트랜잭션이 실행되는 동안 전체 데이터베이스에 독점 잠금을 획득하는 것과 본질적으로 같습니다.
  • 직렬성 스냅숏 격리는 낙관적 동시성 제어 기법입니다.
    • 트랜잭션을 막는 대신 모든 것이 괜찮아질 거라는 희망을 갖고 계속 진행한다는 뜻입니다.
    • 예비 용량이 충분하고 트랜잭션 사이의 경쟁이 너무 심하지 않으면 낙관적 동시성 제어 기법은 비관적 동시성 제어보다 성능이 좋은 경향이 있습니다.
  • SSI는 스냅숏 격리를 기반으로 합니다. 즉, 트랜잭션에서 실행되는 모든 읽기는 데이터베이스의 일관된 스냅숏을 보게 됩니다.
  • SSI는 스냅숏 격리 위에 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트시킬 트랜잭션을 결정하는 알고리즘을 추가합니다.

뒤처진 전제에 기반한 결정#

  • 트랜잭션은 어떤 전데를 기반으로 어떤 동작을 합니다.
  • 애플리케이션이 질의를 실행할 때 데이터베이스는 애플리케이션 로직이 질의 결과를 어떻게 사용할지 모릅니다.
  • 데이터베이스가 질의 결과가 바뀌었는지 알 수 있는 방법은 두가지 상황이 있습니다.
    • 오래된(stale) MVCC 객체 버전을 읽었는지 감지하기 (읽기 전에 커밋되지 않은 쓰기가 발생했음)
    • 과거의 읽기에 영향을 미치는 쓰기 감지하기(읽은 후에 쓰기가 실행됨)

오래된 MVCC 읽기 감지하기#

  • 트랜잭션이 MVCC 데이터베이스의 일관된 스냅숏에서 읽으면 스냅숏 생성 시점에 다른 트랜잭션이 썼지만 아직 커밋되지 않은 데이터는 무시합니다.

트랜잭션이 MVCC 스냅숏에서 뒤처진 값을 읽은지 감지

  • 데이터베이스는 트랜잭션이 MVCC 가시성 규칙에 따라 다른 트랜잭션의 쓰기를 무시하는 경우를 추적합니다.
  • SSI는 불필요한 어보트를 피해서, 일관된 스냅숏에서 읽으며 오래 실행되는 작업을 지원하는 스냅숏 격리의 특성을 유지합니다.

과거의 읽기와 영향을 미치는 쓰기 감지하기#

  • 트랜잭션이 완료되고 동시에 실행되는 모든 트랜잭션들이 완료된 후에 데이터베이스는 트랜잭션에서 어떤 데이터를 읽었는지 잊어버려도 됩니다.
  • 트랜잭션이 데이터베이스에 쓸 때 영향받는 데이터를 최근에 읽은 트랜잭션이 있는지 색인에서 확인해야 합니다.

직렬성 스냅숏 격리의 성능#

  • 트랜잭션에서 다양한 트레이드오프가 있으며 트랜잭션의 읽기 쓰기를 추적하는 세밀함의 정도입니다.
  • 데이터베이스가 각 트랜잭션의 동작을 매우 상세하게 추적하면 어보트돼야 하는 트랜잭션을 정확히 판별할 수 있지만 기록 오버헤드가 심해질 수 있습니다.
  • 데이터베이스가 각 트랜잭션의 동작을 매우 상세하게 추적하면 어보트돼야 하는 트랜잭션을 정확히 판별할 수 잇지만 기록 오버헤드가 심해질 수 있습니다.
  • 2단계 잠금과 비교할 때 직렬성 스냅숏 격리의 큰 이점트랜잭션이 다른 트랜잭션들이 잡고 있는 잠금을 기다리느라 차단될 필요가 없습니다.
  • 순차 실행과 비교할 때 직렬성 스냅숏 격리는 단일 CPU 코어의 처리량에 제한되지 않습니다.
    • 직렬성 충돌 감지를 여러 장비로 분산시켜서 처리량이 아주 높도록 확장할 수 있습니다.
    • 데이터가 여러 장비에 걸쳐서 파티셔닝돼 있더라도 트랜잭션은 직렬성 격리를 보장하면서 여러 파티션으로부터 읽고 쓸 수 있습니다.
  • 어보트 비율은 SSI의 전체적인 성능에 큰 영향을 미칩니다.
    • 오랜 시간 동안 데이터를 읽고 쓰는 트랜잭션은 충돌이 나고 어보트되기 쉬우므로 SSI는 릭기 쓰기 트랜잭션이 상당히 짧기를 요구합니다.
    • SSI는 2단계 잠금이나 순차 실행보다는 느린 트랜잭션에 덜 민감합니다.

정리#

트랜잭션이 애플리케이션이 어떤 동시성 문제와 어떤 종류의 하드웨어와 소프트웨어 결함이 존재하지 않는 것처럼 동작할 수 있게 도와주는 추상층입니다.

모든 애플리케이션이 이런 문제에 민감하지 않으며 단일 레코드만 읽거나 쓰는 것처럼 간단한 경우는 트랜잭션 없이도 관리할 수 있으나, 접근 패턴이 복잡할 때는 트랜잭션이 상상할 수 있는 잠재적인 오류를 줄여줍니다.

  • 트랜잭션이 없으면 다양한 오류 시나리오에서 다양한 방법으로 데이터가 일관성이 깨질 수 있습니다.
  • 트랜잭션이 없다면 복잡한 상호작용을 하는 접근이 데이터베이스에 미치는 영향을 뗘보기가 매우 어렵습니다.

격리 수준들의 특징은 다음과 같습니다.

  • 더티 읽기
    • 한 클라이언트가 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 읽습니다.
    • 커밋 후 읽기 또는 그보다 강한 격리 수준은 더티 읽기를 방지합니다.
  • 더티 쓰기
    • 한 클라이언트가 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 덮어씁니다.
    • 거의 모든 트랜잭션 구현은 더티 쓰기를 방지합니다.
  • 읽기 스큐(비반복 읽기)
    • 클라이언트는 다른 시점에 데이터베이스의 다른 부분을 봅니다.
    • 스냅숏으로부터 읽는 스냅숏 격리를 가장 흔히 사용합니다.
  • 갱신 손실
    • 두클라이언트가 동시에 read-modify-write 주기를 실행합니다.
    • 한 트랜잭션이 다른 트랜잭션의 변경을 포함하지 않은 채로 다른 트랜잭션이 쓴 내용을 덮어써서 데이터가 손실됩니다.
  • 쓰기 스큐
    • 트랜잭션이 무언가를 읽고 읽은 값을 기반으로 어떤 결정을 하고 그 결정을 데이터베이스에 씁니다.
    • 쓰기를 실행하는 시점에서 결정의 전제가 참이 아니며 직렬성 격리만 이런 이상 현상을 막을 수 있습니다.
  • 팬텀 읽기
    • 트랜잭션이 어떤 검색 조건에 부합하는 객체를 읽고, 다른 클라이언트가 그 검색 결과에 영향을 주는 쓰기를 실행합니다.
    • 스냅숏 격리는 간단한 팬텀 읽기는 막아주지만 쓰기 스큐 맥락에서 발생하는 팬텀은 색인 범위 잠금처럼 특별한 처리가 필요합니다.

완화된 격리 수준은 이런 이상 현상 중 일부는 막아주지만 나머지는 애플리케이션 개발자가 수동으로 처리해야합니다. 직렬성 격리만 이 모든 문제를 막을 수 있으며 직렬성 트랜잭션을 구현하는 방법으로 아래 세가지가 있습니다.

  • 트랜잭션을 순서대로 실행
    • 트랜잭션이 실행 시간이 아주 짧고 트랜잭션 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 트랜잭션 처리량이 낮다면 간단하고, 효과적인 선택입니다.
  • 2단계 잠금(2PL)
    • 직렬성을 구현하는 표준적인 방법이였으나 성능 특성 때문에 피하는 애플리케이션이 많습니다.
  • 직렬성 스냅숏 격리(SSI)
    • 앞의 결점 대부분을 피합니다.
    • 낙관적 방법을 사용해서 트랜잭션이 차단되지 않고 진행할 수 있게 합니다.
    • 트랜잭션이 커밋을 원할 때 트랜잭션을 확인해서 실행이 직렬적이지 않다면 어보트 시킵니다.

이번 장에서는 단일 장비에서 데이터베이스를 실행하는 맥락에서 아이디어와 알고리즘을 살펴보았습니다. 다음 두 장에서는 분산 데이터베이스에서의 트랜잭션에서 발생하는 문제에 해결책을 이야기합니다.


Last updated on