지난번에는 제가 조사한 내용을 바탕으로 간략한 역사와 논문이 나오게 된 이유를 알아봤습니다.
해당 분야에 전문가가 아니기 때문에 반드시 참고자료를 확인하시길 바랍니다. 또한 피드백은 적극 환영합니다!
그렇다면 논문에서 제시하는 고립 수준과 이상현상은 무엇일까요? 또한 굳이 알 필요가 있을까요?
제 생각은 '모든 내용을 알 필요는 없지만, 대략적인 내용은 알고 있어야 한다.'입니다. 왜냐하면 DBMS마다 고립 수준을 나타내는 용어도 다르고 그들이 구현한 방법도 다릅니다. 또한 논문이 제시된 이유가 '모호함을 개선'이기 때문에, 이러한 '모호함에서 발생할 수 있는 문제'가 있을 것입니다. 즉, 데이터베이스의 선택 혹은 발생한 문제를 빠르게 이해하는데 도움이 된다고 생각합니다.
1. 개요
저번 글에서도 확인하실 수 있지만, 해당 논문이 나온 배경을 간략하게 설명드리면, '표준이 정의될 때는 MVCC를 구현한 Snapshot Isolation 개념이 없었다. 이후 Snapshot Isolation은 MySQL, PostgreSQL 등 현대 DBMS에 적용이 되었고 이들은 표준의 요구사항을 만족하기 때문에 격리 수준을 SQL92 기준으로 설명한다. 하지만 표준의 정의는 모호하며 이상현상은 3가지 외 더 있다.'입니다.
그렇다면 어떠한 문제가 있는지 살펴보겠습니다.
2. 이상현상
2.1 P0 - Dirty Write
Dirty write는 아무런 제재가 없을 때 발생하는 이상현상입니다. 이 현상은 하나의 트랜잭션(Tx1)이 커밋이 안된 기록(Tx2)을 덮어 씌우는 현상입니다.
단순히 늦게 처리된 작업이 덮어쓰기 때문이라고 생각할 수 있지만, 이보다 더 복잡한 문제가 있습니다.
Tx1이 롤백을 할 때, x는 10이었습니다. 하지만 Tx2가 롤백이 될 때, x는 0이였습니다. 각 트랜잭션은 Isolation을 보장하기 때문에 다른 트랜잭션이 무엇을 처리했는지 알 수가 없습니다.
아무런 제재가 없기 때문에 Rollback이 될 경우 어느 트랜잭션을 기준으로 롤백해야 하는지 알 수가 없습니다. 즉, 트랜잭션에서 제공하는 ACID 중 Consistency(일관성)을 보장할 수 없게 됩니다.
2.2 P1 - Dirty Read
Dirty Read는 SQL92에도 명시된 이상현상이고 '커밋이 안된 트랜잭션이 작업한 내용을 다른 트랜잭션이 읽는 현상'을 뜻합니다.
Tx1이 작업하는 동안, Tx2가 Tx1이 작업한 내용을 도중에 읽었습니다. 즉, x+y=100이 아닌 x+y=60으로 읽은 상태가 됩니다. 이것은 트랜잭션이 처리하는 중에 다른 트랜잭션이 데이터를 읽기 때문에 발생한 현상입니다.
만약 Tx1이 롤백이 된다면 Tx2가 읽은 데이터는 10이 아닌 50이 되어야 합니다. 하지만 이미 커밋된 트랜잭션이기 때문에 일관성을 어기게 됩니다.
일반적으로 'Read committed(커밋 후 읽기)' 단계에서 Dirty write, Drity read를 방지할 수 있습니다.
2.3 P2 - Non-Repeatable Read (read skew)
Non-reaptable read도 SQL92에 명시된 이상현상입니다. Dirty read는 '커밋된 데이터'에 대해서 제약이 없습니다. 그렇다면 커밋되기 전에 읽은 데이터와 커밋 이후에 읽은 데이터에 차이가 발생하면 어떤 문제가 발생할까요?
위 예시에서 x+y = 100인 일관성을 보장해야 합니다. 하지만 Tx1이 처리되는 도중에 Tx2가 데이터를 처리한 뒤 커밋할 경우, Tx1은 x+y=140이 발생하게 됩니다. 즉, 일관성을 어기게 됩니다.
lock 기반으로 구현할 경우 '긴 텀의 읽기, 쓰기 lock을 사용한다. (Tx1이 처리되기 전까지 Tx2가 대기)'로 해결할 수 있습니다. (읽기 연산 후 중간에 쓰기 작업이 수행되었기 때문.) 혹은 '읽기 작업을 위한 별도의 테이블 복제본을 생성해서 일관된 읽기를 보장한다.(MVCC)'로 해결할 수도 있습니다.
2.4 P3- Phantom
Phantom도 SQL92에 명시된 이상현상입니다. 이제까지 여러 트랜잭션들이 같은 데이터를 읽을 경우를 따졌습니다. lock 기반으로 구현할 경우 작업하는 row 기준으로 lock을 걸어서 이상현상을 방지할 수 있습니다.
하지만 단일 row 기준이 아닌 여러 rows가 포함된 연산일 경우 어떻게 해결할까요? 트랜잭션은 고립성을 보장하기 때문에 서로의 상태를 알 수 없습니다. 작업할 row에 lock이 있다면 잠시 대기하다가 작업을 처리할 뿐입니다.
Phantom은 이처럼 범위 질의(select... where)에서 발생하는 이상현상입니다. lock 기반으로 구현할 경우 '범위 검색에 포함된 rows들을 잠근다.'로 해결할 수 있습니다.
2.5 P4 - Lost Update
Lost update는 논문에서 추가된 이상현상입니다. 이 현상은 Non-Repeatable Read와 비슷하게 커밋된 데이터에 제약이 없어 발생하는 문제입니다. 즉, '커밋이 된 데이터'를 다시 읽을 때 나중에 쓴 트랜잭션이 이전 결과를 덮어버리는 경우가 발생합니다.
이것은 Dirty write와 차이가 없다고 생각할 수 있습니다. 하지만 Dirty write는 '커밋이 안된' 데이터를 덮어버리는 경우 발생하는 현상입니다. 그래서 '쓰기 작업에 lock'을 거는 것으로 해결할 수 있었습니다. Lost update는 위 Dirty write의 현상을 이미 방지한 상황에서 발생하는 현상입니다.
lock 기반으로 구현할 경우, row을 작업하는 트랜잭션이 row에 대한 잠금을 독점하는 것으로 방지할 수 있습니다.
MySQL InnoDB는 Lost update를 감지하지 않습니다. 즉, 먼저 쓰인 데이터는 손실될 가능성이 있습니다. 고립 수준은 Serializable으로 높이거나 select for update를 사용해서 조회 시에도 lock을 사용하는 것으로 방지할 수 있습니다.
2.6 P4C - Cursor Lost Update
Cursor는 메모리에 저장된 특정 실행 결과를 가리키는 포인터입니다. Cursor에 저장된 결과 값이 데이터를 덮어쓰는 현상을 Cursor lost update라고 합니다.
lock 기반으로 구현할 경우 Cursor는 작업을 위한 Open, Close 연산이 있는데, 해당 연산을 실행하는 시점에 읽은 데이터들 모두 lock 걸어 두는 것으로 해결할 수 있습니다.
2.7 A5A - Read Skew
Read skew는 2개 이상의 데이터를 조회했을 때, 데이터들의 일관성이 깨지는 이상현상입니다.
A Critique of ANSI SQL Isolation Levels 6페이지에서는 Read Skew를 2개의 데이터를 읽을 때 발생하는 것으로 설명합니다.
A5A Read Skew Suppose transaction T1 reads x, and then a second transaction T2 updates x and y to new values and commits.
이 현상은 Non-repeatable read와 유사하게 보입니다. 일반적으로 Non-repeatable read는 1개의 row를 읽었을 때 발생하는 때, Read skew는 2개의 데이터를 읽을 때를 설명하는 경우가 있습니다. 일부에서는 'Read skew는 non-repeatable read의 일반화된 경우이다.'라고 설명하는 곳도 있고 'Read skew와 non-repeatable read는 동일한 현상이다.'라고 설명도 있었습니다.
이런 현상을 비반복 읽기(Non-repeatable read) 또는 읽기 스큐(Read skew)라고 부른다.
- 데이터 중심 애플리케이션 설계 07장. 트랜잭션 p237 -
MySQL는 이 현상을 undo log를 통해서 방지하고 있습니다. 트랜잭션은 각자 다른 버전의 테이블을 바라보기 때문에 단일, 여러 rows를 읽어도 read skew, non-repeatable read를 방지할 수 있습니다.
2.8 A5B - Write Skew
Write skew는 좀 미묘한 이상현상입니다. 이 현상은 dirty write, lost update가 아니지만 쓰기 작업에서 발생하는 것입니다.
위 예시에서 병원에는 반드시 1명의 의사가 있어야 합니다. 우연히 Bob과 Alice가 동시에 퇴근을 눌렀고 두 트랜잭션 모두 정상 처리가 되었습니다. 이때 일관성을 어기게 됩니다.
MySQL InnoDB의 Repeatable read는 이 현상을 방지할 수 없습니다. 고립 수준은 Serializable으로 올린다면 방지할 수는 있으나, 높은 수준의 고립 단계는 빈번한 락을 유발하기 때문에 동시성이 떨어지며, 데드락이 발생할 수 있습니다.
3. Lost Update, Write Skew는 어떻게 해결할 수 있을까?
병원을 예시로 사용했지만, 만약 재고에 관련된 문제라면? 예약 시스템에서 발생하는 문제라면? 퇴근한 의사는 다시 호출하면 돌아오겠지만, 재고와 예약 시스템에서 발생한 문제는 금전적 보상 때문에 굉장히 복잡할 것입니다. 이 문제를 해결할 방법은 다양하게 있는데 그것을 다음 포스트에서 확인해 보도록 하겠습니다.
참고자료
A Critique of ANSI SQL Isolation Levels
https://blog.acolyer.org/2016/02/24/a-critique-of-ansi-sql-isolation-levels/
MySQL 8.0 Reference Transaction Isolation Levels
A beginner’s guide to Read and Write Skew phenomena
Abraham Silberschatz, Henry F. Korth, S. Sudarshan, 데이터베이스 시스템, 정연돈, 권준호 역 외 3명, (한빛아카데미, 2021)
백은빈, 이성욱 저, Real MySQL 1권 개발자와 DBA를 위한 MySQL 실전 가이드, (위키북스, 2021)
Martin Kleppmann, 데이터 중심 애플리케이션 설계, 정재부, 김영준, 이도영 역, (위키북스, 2018)
'데이터베이스' 카테고리의 다른 글
ANSI SQL-92에 대한 비평 (원문 : A Critique of ANSI SQL Isolation Levels) - 용어, 배경 설명 (0) | 2022.09.17 |
---|---|
Thread Pool과 Connection Pool은 어떤 관계가 있고 적당한 크기는 얼마일까? (0) | 2022.09.15 |
MySQL Repeatable Read는 어떻게 Phantom을 막을까? (Next-Key lock) (0) | 2022.08.14 |
왜 트랜잭션 격리 수준을 나눴을까? (0) | 2022.07.24 |