본문 바로가기
스터디/데이터 중심 애플리케이션 설계

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

by 디토20 2023. 1. 2.
반응형

 

 

 

 

 

 

 

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

 

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

1.1 ACID

ACID는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.

1.1.1 원자성(Atomicity)

  • 원자적이란 더 작은 부분으로 쪼갤 수 없는 것을 가리킨다.
  • 시스템은 연산을 실행하기 전이나 실행한 후의 상태에만 있을 수 있으며 그 중간 상태에는 머물 수 없다.
  • 원자성 덕분에 실패한 트랜잭션은 어떤 것도 변경하지 않았음을 알 수 있으므로 안전하게 재시도 할 수 있다.

1.1.2 일관성(Consistency)

  • 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 말한다.
  • 일관성(C)은 실제로는 ACID에 속하지 않고 애플리케이션의 속성으로 본다.
  • 데이터베이스 자체만으로 불변식을 위반하는 잘못된 데이터를 쓰지 못하도록 막을 수 없기 때문이다.
  • 이러한 것은 애플리케이션의 책임으로 보고 일관성을 달성하기 위해 데이터베이스의 원자성과 격리성 속성에 기댈 수 있다.

1.1.3 격리성(Isolation)

  • 동시에 실행되는 트랜잭션은 서로 격리되어 방해할 수 없다.
  • 한 트랜잭션이 여러 번 쓴다면 다른 트랜잭션은 그 내용을 전부 볼 수 있든지 아무것도 볼수 없든지 둘 중 하나여야 하고 일부분만 볼 수 있어서는 안된다.

1.1.4 지속성(Durability)

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

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

2.1 단일객체 쓰기

  • 원자성과 격리성은 단일 객체를 변경하는 경우에도 적용된다.
    • 한 객체의 데이터를 끊어 보낼 때 네트워크 연결이 끊기면 조각 데이터를 저장할 것인가?
    • 문서를 쓰고 있을 때 다른 클라이언트에서 그 문서를 읽으면 부분적으로 갱신된 값을 읽게 될까?
  • 그렇기에 저장소 엔진들은 거의 보편적으로 한 노드에 존재하는 단일 객체 수준에서 원자성과 격리성을 제공하는 것을 목표로 한다.

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

  • 다중 객체 트랜잭션은 여러 파티션에 걸쳐서 구현하기가 어렵고 매우 높은 가용성과 성능이 필요한 곳에서는 방해가 되기도 한다.
  • 다중 객체 트랜잭션은 데이터의 여러 조각이 동기화된 상태로 유지돼야 할 때 필요
  • 트랜잭션이 없더라도 복잡한 데이터의 쓰기와 읽기를 수행하는 애플리케이션을 구현할 수 있다.
  • 그러나 원자성이 없으면 오류 처리가 훨씬 더 복잡해지고 격리성이 없으면 동시성 문제가 생길 수 있다.
  • 다중 객체 트랜잭션은 참조가 유효한 상태로 유지되도록 보장해준다.(외래키 등 참조)
  • 비정규화된 데이터가 동기화가 꺠지는 것을 방지(한번에 여러 문서 갱신 시)
  • 트랜잭션 격리성이 없으면 어떤 색인에서는 레코드가 보이지만 다른 색인은 아직 갱신되지 않아서 레코드가 보이지 않을 수 있다.

3. 완화된 격리 수준

3.1 커밋 후 읽기 (READ COMMITTED)

가장 기본적인 수준의 트랜잭션 격리로 이 수준에서는 두 가지를 보장해 준다.

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

3.1.1 더티 읽기 방지

더티 읽기(dirty read) : 어떤 트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상

더티 읽기를 막아야 하는 이유

  • 더티 읽기가 생기면 다른 트랜잭션이 일부는 갱신된 값을, 일부는 갱신되지 않은 값을 볼 수 있다.
  • 트랜잭션이 어보트되면 모두 롤백되어야 하나, 더티 읽기를 허용하면 트랜잭션이 나중에 롤백될 데이터를 볼 수 있다.

3.1.2 커밋 후 읽기 구현

커밋 후 읽기는 Oracle 11g, PostgreSQL, SQL Server 2012, MemSQL 등에서 기본 설정으로 쓰고 있는 격리 수준이다.

  • 더티 쓰기 방지 : 트랜잭션이 커밋되거나 어보트될 때까지 잠금을 보유한다. 이런 잠금은 커밋 후 읽기 모드에서 데이터베이스에 의해 자동으로 실행된다.
  • 더티 읽기 방지: 과거의 커밋된 값/ 현재 쓰고 있는 새로운 값을 모두 기억하게 하여 해당 트랜잭션이 실행 중인 동안 과거의 값을 읽게하여 더티 읽기를 방지 할 수 있다.

더티 읽기 방지: 사용자 2는 사용자 1의 트랜잭션이 커밋된 후에야 x의 새 값을 보게 된다.

3.2 스냅숏 격리와 반복 읽기(Snapshot Isolation)

읽기 스큐: 앨리스는 일관성이 깨진 상태인 데이터베이스를 본다.

 

  • 커밋 후 읽기 격리 수준에서도 동시성 버그가 생길 수 있으며 이런 현상을 비반복 읽기(nonrepeatable read)읽기 스큐(read skew)라고 한다.
  • 위와 같은 경우 몇 초 후 새로고침하면 일관성 있는 계좌를 볼 수 있으나 어떤 상황에서는 이런 비일관성을 감내할 수 없는 경우도 있다.
    • ex) 백업 (원본과 복사본의 데이터 차이), 분석 질의와 무결성 확인(큰 부분을 스캔하는 질의시 다른 시점의 데이터베이스 일부를 보게되면 잘못된 결과 반환)
  • 스냅숏 격리 : 각 트랜잭션은 데이터베이스의 일관된 스냅숏으로부터 데이터를 읽는다.
  • 즉 트랜잭션은 시작할 때 데이터베이스에 커밋된 상태였던 모든 데이터를 본다. 데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 볼 뿐이다.

3.2.1 스냅숏 격리 구현

다중 버전 동시성 제어(multi-version concurrency control, MVCC) : 데이터베이스가 객체의 여러 버전을 함께 유지하는 기법

  • 핵심 원리 : 읽는 쪽에서 쓰는 쪽을 결코 차단하지 않고, 쓰는 쪽에서는 읽는 쪽을 결코 차단하지 않는다.

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

트랜잭션은 데이터베이스에서 객체를 읽을 때 트랜잭션 ID를 사용해 어떤 것을 볼 수 있고 어떤 것을 볼 수 없는지 결정한다.

  • 트랜잭션 ID가 더 큰(즉 현재 트랜잭션이 시작한 후에 시작한) 트랜잭션이 쓴 데이터는 그 트랜잭션의 커밋 여부에 관계 없이 모두 무시된다.

3.2.3 색인과 스냅숏 격리

다중 버전 데이터베이스에서 색인의 동작

  • 하나의 선택지는 색인이 객체의 모든 버전을 가리키게 하고 색인 질의가 현재 트랜잭션에서 볼 수 없는 버전을 걸러내고, 가비지 컬렉션이 어떤 트랜잭션에게도 더 이상 보이지 않는 오래된 객체 버전을 삭제 할때 대응되는 색인 항목도 삭제

4. 갱신 손실 방지

  • 갱신 손실 : 두 트랜잭션이 작업을 동시에 하면 두번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경 중 하나는 손실될 수 있음

해결책

  • 원자적 쓰기 연산
  • 명시적인 잠금
  • 갱신 손실 자동 감지
  • Compare-and-set

4.1 원자적 쓰기 연산

  • 쓰기 연산에 원자성 (Atomicity) 성질을 부여함으로서 동시성 안전 획득
  • exclusive lock 을 획득하여 구현 → 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못함
  • or 모든 원자적 연산을 단일 스레드에서 실행되도록 강제하는 방법

4.2 명시적인 잠금

  • 애플리케이션에서 갱신할 객체를 명시적으로 잠그는 것
  • 다른 트랜잭션이 동시에 같은 객체를 읽으려고 하면 첫 번째 read-modify-write 주기가 완료될 때까지 기다리도록 강제됨
BEGIN TRANSACTION;

SELECT * FROM figures
  WHERE name = 'robot' AND game_id = 222
  FOR UPDATE; (1)

-- 이동이 유효한지 확인한 후
-- 이전의 SELECT에서 반환된 것의 위치를 갱신한다.
UPDATE figures SET position = '4' WHERE id = 1234;

COMMIT;

4.3 갱신 손실 자동 감지

  • 여러 트랜잭션의 병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 abort 시키고, 재시도하도록 강제하는 방법

4.4 Compare-and-set

  • 값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용함으로써 갱신 손실을 회피하는 것
-- 데이터베이스 구현에 따라 안전할 수도 안전하지 않을 수도 있다
UPDATE wiki_pages SET content = 'new content'
  WHERE id = 1234 AND content = 'old content';

5. 쓰기 스큐와 팬텀

  • 거의 동시에 두 트랜잭션이 시작되었다고 가정

  • 데이터베이스에서 스냅숏 격리를 사용하므로 둘 다 2를 반환해서 두 트랜잭션 모두 다음 단계로 진행함
  • 최소 한 명의 의사가 호출 대기해야 한다는 요구사항 위반
  • 이러한 현상을 쓰기 스큐 (wirte skew) 라고 함

5.1 쓰기 스큐를 특정 짓기

  • 쓰기 스큐는 두 트랜잭션이 같은 객체들을 읽어서 그 중 일부를 갱신할 때 나타날 수 있음

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

  • 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 것을 팬텀(Phantom) 이라고 함
  • 스냅숏 격리는 읽기 전용 질의에서는 팬텀을 회피하지만 읽기 쓰기 트랜잭션에서는 팬텀이 발생할 수 있음

5.3 충돌 구체화

  • 최초의 select 시 잠글 수 있는 객체가 없어 팬텀이 발생한다면 인위적으로 데이터베이스에 잠금 객체를 추가하자
  • 대상 row 를 미리 만들고 lock 을 건다 → 트랜잭션 대상이 되는 특정 범위의 모든 조합에 대해 미리 row 를 만들어 둠 (ex, 회의실 예약의 경우 다음 6개월 동안에 해당되는 양)
  • 예약을 하는 트랜잭션은 테이블에서 원하는 대상 row 를 잠글 수 있음 (위에서 미리 생성했기 때문에)
  • 여기서 생성된 row 는 단지 동시에 변경되는 것을 막기 위한 잠금의 모음일 뿐임 (실제 사용되는 데이터가 아님)
  • 동시성 제어 메커니즘이 애플리케이션 데이터모델로 새어 나오는 것은 보기 좋지 않음, 다른 대안이 불가능할 때 최후의 수단으로 고려

6. 직렬성

  • 직렬성 격리는 보통 가장 강력한 경리 수준이라고 여겨짐
  • 여러 트랜잭션이 병렬로 실행되더라도, 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장

직렬성을 제공하는 3가지 기법

  • 말 그대로 트랜잭션을 순차적으로 실행하기
  • 2단계 잠금
  • 직렬성 스냅숏 격리 같은 낙관적 동시성 제어 기법

6.1 실제적인 직렬 실행

  • 동시성 문제를 피하는 가장 간단한 방법은 동시성을 완전히 제거하는 것
  • 한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하면 됨
  • 단점 → 성능

6.1.1 직렬 실행의 제약 사항

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

  • 모든 트랜잭션은 작고 빨라야 한다. 느린 트랜잭션 하나가 전체 처리를 지연시킬 수 있기 때문.
  • 활성화된 데이터셋이 메모리에 적재될 수 있는 경우로 사용이 제한됨, 단일 스레드 트랜잭션에서 디스크에 접근한다면 시스템이 매우 느려짐
  • 쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 충분히 낮아야 함
  • 여러 파티션에 걸친 트랜잭션도 쓸 수 있지만, 이것을 사용할 수 있는 정도에는 엄격한 제한이 있음

6.2 2단계 잠금(2PL)

  • 트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 그 객체에 쓰기를 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다(이렇게 하면 B가 A 몰래 갑자기 객체를 변경하지 못하도록 보장된다).
  • 트랜잭션 A가 객체에 썼고 트랜잭션 B가 그 객체를 읽기 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다
  • vs 스냅숏 격리(읽는 쪽은 결코 쓰는 쪽을 막지 않으며, 쓰는 쪽도 결코 읽는 쪽을 막지 않음)
  • 2PL은 직렬성을 제공하므로 스냅숏 격리에서 발생할 수 있는 갱신 손실이나 쓰기 스큐를 포함한 모든 경쟁 조건으로부터 자유롭다.

6.2.1 2단계 잠금 구현

  • MySQL(InnoDB), SQL Server 에서 직렬성 격리 수준을 구현하는데 사용됨
  • 잠금은 공유 모드 (shared mode) 나 독점 모드 (exclusive mode) 로 사용될 수 있음
  • 잠금이 아주 많이 사용되므로 교착 상태(두 개의 트랜잭션이 서로 기다리는 것)가 매우 쉽게 발생할 수 있음

6.2.2 2단계 잠금의 성능

  • 가장 큰 약점이 성능
  • 잠금을 획득하고 해제하는 오버헤드 때문에 느린 것
  • 더 중요한 원인은 동시성이 줄어들기 때문 (동시성과 성능은 반비례)

7. 직렬성 스냅숏 격리 (Serializable Snapshot Isolation, SSI)

  • 직렬성 격리와 좋은 성능은 공존할 수 있을까? - 직렬성 스냅숏 격리
  • 현재 최고로 유망한 것이 직렬성 스냅숏 격리
  • 스냅숏 격리에 비해 약간의 성능 손해만 있을 뿐임

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

  • 2단계 잠금은 비관적 동시성 제어 메커니즘
    • 뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때 까지 기다리는게 낫다는 원칙
  • 직렬성 스냅숏 격리는 낙관적 동시성 제어 메커니즘임
    • 뭔가 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 모든 것이 괜찮아질 거라는 희망을 갖고 계속 진행한다는 뜻
    • 트랜잭션이 커밋되기를 원할 때 데이터베이스는 나쁜 상황이 발생했는지 확인함
    • 발생했다면 abort 되고 재시도함
    • 경쟁이 심하면 abort 비율이 높아지므로 성능 떨어짐
    • 예비 용량이 충분하고 트랜잭션 사이의 경쟁이 너무 심하지 않으면, 낙관적 동시성 제어 기법이 성능이 좋은 경향이 있음
    • SSI = 스냅숏 격리 + 직렬성 충돌 감지 및 abort 시킬 트랜잭션 결정하는 알고리즘

 

 

 

 

728x90
반응형

댓글