* 이 글은 [데이터 중심 애플리케이션 설계]를 공부하며 기록을 남긴 것입니다.
트랜잭션
- 다수의 읽기와 쓰기(다중 객체에 대한 다중 연산)를 하나의 논리적 단위로 묶는 방법 → 전체가 성공하거나 실패함
- 실패나 오류를 처리하기 쉬워짐: 안전성 보장(ACID)
- 애플리케이션에서 DB에 접근하는 프로그래밍을 단순화하기 위한 목적
- 트랜잭선의 개념은 모호해졌고, 이점과 한계가 명확하다!
1. ACID
- 원자성 Atomicity
- 여러 작업을 하나의 트랜잭션으로 묶어 처리하는 것
- 오류가 생기면 전체를 abort하고 안전하게 재시도한다. 트랜잭션의 핵심 기능!
- 여담으로.. Django는 이런 abort 트랜잭션을 재시도 하지 않아서, 오류가 생기면 요청은 사라지고 사용자는 오류만 받게된다. Django로 작업할 때 유독 에러 때문에 힘들었는데 그래서 그랬구나..
- 하지만 네트워크, 과부하, 일시적인 오류 등의 상황에서는 이런 재시도 매커니즘도 좋지 않을 수 있다
- 일관성 Consistency
- 데이터에는 항상 만족해야 하는 불변식이 있으며, 이를 위반하는 데이터는 저장될 수 없다는 것
- 하지만 이는 데이터베이스에서 보장할 수 없는 애플리케이션 단의 문제이다. 따라서 실제로는 ACID에 속할 필요가 없다는 의견도 있다
- 격리성 Isolation (=직렬성)
- 여러 트랜잭션이 동시에 실행되더라도 순차적으로 실행된 것과 같은 결과를 보장하는 것
- 직렬성 격리는 성능 손해를 유발하기 때문에 실제로는 스냅숏 격리를 사용
- 지속성 Durability
- 트랜잭션에서 기록한 모든 데이터는 손실되지 않음을 보장하는 것
- 복제를 통해 보장하지만 완벽한, 유일한 방법은 없다
2. 다중 객체 트랜잭션
- 단일 객체 연산: 대부분의 저장소 엔진은 한 노드에 존재하는 단일 객체 수준에서 원자성/격리성을 보장하는 것을 목표로 한다. 이는 동시에 같은 객체에 쓰는 문제 등의 해결에 유용하지만, 일반적인 의미의 트랜잭션은 아니다.
- 다중 객체 연산: 한번에 여러 객체(로우, 문서, 레코드, 색인 등)를 변경하는 것
- 하지만 구현하기 어렵고 가용성과 성능에 방해될 수 있어 많은 분산 데이터베이스에서는 다중 객체 트랜잭션을 지원 하지않는다
- 개념적으로는 분산 DB에서 트랜잭션을 막는 것이 없다? → 9장: 분산 트랜잭션의 구현
- 그럼 정말 필요한 것일까?? 단일 객체만으로도 충분한 경우도 있지만, 아닌경우 오류처리나 동시성에 문제가 생길 수 있다
완화된 격리 수준
- 동시성 문제(경쟁 조건): 두 트랜잭션이 동시에 데이터를 변경하려고 하거나, 동시에 변경한 데이터를 읽으려고 할 때 나타난다
- 테스트로 발견하기 어려움, 재현하기 어려움, 어느 부분에서 문제인지도 추론하기 어려움
- 트랜잭션 격리를 통해 해결
- 대부분 완전한 직렬성 격리보다는 완화된 격리 수준(=비직렬성)을 사용한다
- 도구에 의존하기 보다 발생할 수 있는 동시성 문제를 잘 이해하고 방지하는 방법을 익힐 필요가 있다!
1. 커밋 후 읽기
- 더티 읽기/쓰기 방지 → 커밋된 데이터만 읽을 수 있고, 커밋된 데이터만 덮어쓸 수 있다.
- 구현
- 더티 쓰기 방지: 로우(객체) 수준 쓰기 잠금
- 더티 읽기 방지(main): 과거의 데이터와 쓰기 잠금을 가지고 있는 트랜잭션에서 새로 쓴 값을 모두 기억하고, 그 사이에 데이터를 읽으려는 작업은 과거의 데이터를 읽도록 한다
- 비반복 읽기(읽기 스큐) 문제 발생 가능 → 2. 스냅숏 격리
- 순차적인 카운터 증가 상황과 같은 갱신 손실 문제 발생 → 3. 갱신 손실 방지
2. 스냅숏 격리
트랜잭션 처리 중 중간에 읽기를 수행하면, 일부는 처리되고 일부는 처리되지 않은 것 처럼 보일 수 있다. 이런 현상을 읽기 스큐(read skew)라고 한다. 여기서 skew는 '핫스팟이 생긴 불균형적인 작업부하'가 아닌 '시간적인 이상 현상'을 의미한다. 이 문제의 일반적인 해결책으로 스냅숏 격리를 사용한다.
- 트랜잭션은 작업을 시작할 때 데이터베이스의 특정 스냅숏으로부터 데이터를 읽는다. 따라서 특정 시점에 고정된, 일관된 데이터만을 읽어올 수 있게 된다.
- 핵심 원리: 읽는 작업과 쓰는 작업은 서로를 결고 차단하지 않는다 → 쓰기 작업은 쓰기 작업만 차단하면 된다
- 구현: 쓰기 잠금 + 다중 버전 객체를 사용한 다중 버전 동시성 제어(MVCC)
- 여러 트랜잭션에서 다른 시점의 데이터베이스를 읽어야 할 수 있으므로, 객체마다 커밋된 버전 여러개를 모두 저장해둔다. 생성/삭제는 여러 버전일 때 충돌 할 수 있는데, created_by/deleted_by column을 사용해 해결함
- 시점에 따라 데이터베이스에서 읽을 수 있는 데이터가 달라지는데 스냅숏 시점 이전에 커밋된 데이터/삭제 커밋이 완료되지 않은 데이터가 질의 대상이 된다.
- 다중 버전 DB에서의 색인은?
3. 갱신 손실 방지
갱신 손실: 카운터 증가(read-modify-write)와 같은 여러 쓰기가 동시에 수행되는 경우, 시점에 따라 일부 변경사항이 손실될 수 있다. 흔한 문제라 다양한 해결책이 있다.
1. 원자적 쓰기 연산
데이터베이스에 내장된 원자적 갱신 연산을 사용. RDB에서는 다음과 같이 사용한다.
UPDATE counters SET value = value + 1 WHERE key = 'foo';
NoSQL에서는 문서를 지역적으로 변경하는 연산, Redis에서는 우선순위 큐같은 데이터를 변경하는 연산을 제공한다.
일반적으로 내부는 잠금을 사용하여 구현한다.
2. 명시적인 잠금
애플리케이션 코드에서 명시적으로 객체를 잠근다. 로직을 신중하게 짜야한다.
BEGIN TRANSACTION;
SELECT * FROM figures
WHERE ...
FOR UPDATE; /*WHERE 질의에 해당하는 모든 row에 잠금을 건다*/
UPDATE figures SET ...; /*새로운 상태로 갱신한다*/
COMMIT;
3. 갱신 손실 자동 감지
잠금 없이, 트랜잭션을 병렬로 실행하다가 손실이 감지되면 트랜잭션 관리자가 read-modify-write 주기를 재시도하는 방법이다.
스냅숏 격리와 함께 효율적으로 사용할 수 있고, 애플리케이션 코드에서 데이터베이스 기능을 사용할 필요가 없어진다.
4. Compare-and-set
Compare-and-set 연산은 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용한다. 하지만 스냅숏에 따라 마지막 데이터가 일정하지 않을 수 있으므로 안전하지 않을 수 있다.
UPDATE wiki SET content = 'new content'
WHERE id = 1234 AND content = 'old content' /*내용이 이미 갱신되어 old content가 아니라면, new content 갱신은 적용되지 않는다.*/
5. 충돌 해소와 복제
복제가 적용되어 여러 노드에 데이터의 복사본이 있으면, 다른 노드에서도 변경이 일어날 수 있다.
이런 경우 일반적으로는, 여러 sibling을 생성하도록 하고 사후에 적절히 병합한다.
→ 더 자세히는?
4. 쓰기 스큐와 팬텀
두 트랜잭션이 두 개의 다른 객체를 동시에 갱신할 때, 공통된 경쟁 조건을 위배하게 되는 상황을 쓰기 스큐(write skew)라고 한다. 책에서는 두 의사의 대기 명단 갱신을 예로 들었다. 같은 객체가 아니므로 더티 쓰기나 갱신 손실에 해당되지 않는다.
- 회의실 예약 시스템, 다중플레이어 게임, 사용자명 획득, 이중사용 방지
/* 호출대기 예시. 다른 row들을 잠금하여 해결 */
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;
/* 회의실 예시. row가 없기 때문에 잠금을 못함 */
BEGIN TRANSACTION;
SELECT COUNT(*) FROM bookings
WHERE room_id = 123 AND
end_time > A AND start_time < B
INSERT INTO bookings
(room_id, start_time, end_time, user_id)
VALUES (123, A, B, 666);
COMMIT;
팬텀
- 한 트랜잭션의 쓰기가 다른 트랜잭션의 질의 결과를 바꾸는 효과(위와 같이 없던 row를 추가하는 등)
- 잠글 수 있는 객체가 없다면 데이터베이스에 잠금 객체를 추가하자!
- 하지만 동시성 제어 매커니즘이 모델에 포함되는 것은 좋지 않고
- 대부분은 직렬성을 사용하는 것이 훨씬 좋다!
'데이터 엔지니어링' 카테고리의 다른 글
[DEVIEW 리뷰] Luft: 10초만에 10억 데이터를 쿼리하는 데이터스토어 개발기 (0) | 2021.01.26 |
---|---|
[논문리뷰] MapReduce: Simplefied Data Processing on Large Clusters (0) | 2021.01.24 |
[논문리뷰 시작] Piranha: Optimizing Short Jobs in Hadoop (0) | 2021.01.18 |
[데이터 중심 애플리케이션 설계] 6장 파티셔닝 (0) | 2020.08.07 |
[데이터 중심 애플리케이션 설계] 5장 복제 (0) | 2020.07.29 |