<개요>
- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.
- 이전글에서 Weak Isolation Levels 중 Read Commited, Snapshot Isolation(Repeatable Read)에 관련된 내용을 살펴보았고
이번 글에서는 Preventing Lost Updates, Write Skew (Phantoms) 에 대해서 살펴본다.
- 이전글 https://icthuman.tistory.com/entry/Transactions-2
<내용>
Weak Isolation Levels
Preventing Lost Updates
- read commited 와 snapshot isolation은 주로 동시 쓰기가 있는 경우, 읽기전용 트랜잭션의 볼수 있는 내용을 보장하는 것이다.
- 이전 글에서는 두 트랜잭션이 동시에 쓰는것에 대해서는 다루지 않았고, 특정 유형의 쓰기-쓰기 에 대해서만 살펴봤다.
- 몇 가지 다른유형의 충돌을 살펴 볼텐데 그 중 가장 유명한 것이 Lost Updates 이다.
- 아래 그림을 살펴보자.
TimeLine | 1 | 2 | 3 | 4 |
User #1 | getCounter | 42 + 1 | setCounter 43 | |
Data | 42 | 42 | 43 | 43 |
User #2 | getCounter | 42 + 1 | setCounter 43 |
- 두 Client간의 race condition이 발생하였다. 주로 App이 read - modify - write의 사이클을 가지고 있을때 발생한다.
- 즉 write가 발생하기 전에 그 사이에 일어난 modification을 포함하지 않기 때문이다.
Solutions
Atomic write Operations
- 많은 DB가 atomic update operations를 제공하기 때문에 App에서 해당 로직을 구현하지 않는 것이 좋다. 일반적인 Best Solution
예) UPDATE counters SET val = val + 1 WHERE key = 'foo';
- 일반적으로 DB내부에서 execlusive lock을 통해서 구현하기 때문에 (update할때 읽을 수 없다!) "Cursor Stability 유지"
- 다른 방법으로 all atomic operations를 single thread 에서 수행하는 방법도 있다. (성능 고려)
Explicit Locking
- DB에서 해당 기능을 제공하지 않는다면 App에서 명시적으로 update 될 object를 잠그는 방법이다.
- 잠금을 수행후 read - modify - write 를 수행할 수 있으며, 다른 트랜잭션이 같은 object 에 접근할 경우에 첫 번째 read - modify - write 가 완료될 때 까지 강제로 대기한다.
예 )
BEGIN TRANSACTION
SELECT * FROM ... WHERE ... FOR UPDATE; (해당쿼리로 반환된 all rows lock !)
UPDATE ...
COMMIT;
Automatically detecting lost updates
- atomic operations & locks : read - modify - write 를 순차적으로 하여 lost updates 를 막는다.
- 대안 : 병렬 수행을 허락하고, Transaction Manager 가 lost update를 감지하면, 트랜잭션을 중단하고 강제로 read - modify - write 를 retry 한다!
- 장점 : DB가 이 검사를 효율적으로 수행할 수 있다는 것. with Snapshot Isolation
PostgreSQL | repeatable read | automatically detect and abort the offending transaction |
Orale | serializable | |
SQL | snapshot isolation | |
MySQL, InnoDB | repetable read | X |
Compare-and-Set
- 우리가 CAS연산이라고 부르는 방법이다. DB가 Transcations를 제공하지 않는 atomic compare-and-set을 찾는 것이다. (Single-object writes)
- 마지막으로 값을 읽은 후 값이 변경되지 않았을때에만 업데이트가 발생할 수 있도록 허용하는 것이다.
- 만약 변경이 일어났다면? read-modify-write연산을 재시도한다. 반드시!
주의! Conflict replication
- Locks and Compared and Set은 Single up-to-date, copy of the data를 가정한다.
- replicated DB에서는 여러 노드에 복사본이 존재하고, 데이터 수정이 다른 노드에서 발생할 수 있기 때문에 다른차원의 접근이 필요하다. 즉, 다시 말하면 multi leader 또는 leaderless replication에서는 write가 동시에 발생하고, 비동기 연산이 있다면 보장할 수 없다. (Linearizability)
- 대신 "Detecting Concurrent Writes" 챕터에서 살펴본 내용처럼 concurrent writes 가 충돌된 값의 버전들을 생성하고 (App 또는 별도의 자료구조활용), 이러한 충돌을 versions를 통해서 reslove , merge하는 방법이 가능하다.
- Atomic Operations는 영향을 받지 않는다. (특히 Commutative한 Actions이라면 !)
- 슬프게도.. 많은 replicated DB에서는 기본값으로 Last Write Wins 이다.
<정리>
- 개인적으로 매우 유익했던 챕터이다. 결국 두 개 이상의 동시쓰기가 발생한다면 해결방법은 아래와 같이 정리할 수 있다.
1) 해당 사이클을 통째로 묶는다.
2) 동시수행을 제한한다. ( Lock or Single Thread )
3) 일단 진행시켜! 에러나면 다시 시도
- 멀티 노드를 가지는 Database라면 여러 곳에서 동시다발적으로 데이터에 대한 복제 / 연산이 일어나기 때문에
1) Single Leader를 통해서 제어하던지(Hbase 같은)
2) 마지막에 Write한 값으로 저장
3) 별도의 Application이나 자료구조를 활용하여 충돌버전을 관리하고 resolve / merge
- 그래서 대부분의 분산병렬처리 오픈소스 진영에서 zookeeper를 사용하고 있는 듯 하다.
- 다음 글에서는 이 글에서 다루지 못한 Isolation Level ( Write Skew, Phantoms read )을 좀 더 자세히 살펴보고 분산환경의 Consistency 에 대해서 정리하도록 해야겠다.
'Software Architecture' 카테고리의 다른 글
Designing Data-Intensive Applications - The Trouble with Distributed Systems #1 (0) | 2023.01.13 |
---|---|
HTTP Cache #1(문서의 나이와 캐시 신선도) (0) | 2022.12.29 |
Designing Data-Intensive Applications - Transactions #2 (Atomicity, Isolation) (0) | 2022.05.12 |
Designing Data-Intensive Applications - Transactions #1 (Basic) (0) | 2022.04.14 |
Redis를 활용한 캐시 사용시 주의점 (0) | 2022.03.22 |