SQL 트랜잭션 - 믿는 도끼에 발등 찍힌다
RDBMS를 쓰는 이유 중 하나는 트랜잭션입니다. 하지만 RDBMS의 트랜잭션을 너무 믿다가는 깜짝 놀랄 일이 벌어질 수도 있습니다.

문제
다음과 같이 얼핏 보면 무해해 보이는 코드가 있습니다.
# CREATE TABLE account (id integer, money integer, state text);
# INSERT INTO account (id, money, state) VALUES (1, 10, 'poor');
tx = begin()
state = tx.query("SELECT state FROM account WHERE id = 1")
if state == "poor":
tx.query("UPDATE account SET state = 'rich', money = money * 1000 WHERE id = 1")
tx.commit()
이런 코드가 동시에 다음 순서로 실행되면 어떤 일이 벌어질까요?
트랜잭션 A | 트랜잭션 B |
---|---|
BEGIN | |
SELECT state FROM account WHERE id = 1 | |
BEGIN | |
SELECT state FROM account WHERE id = 1 | |
UPDATE account SET state = 'rich', money = money * 1000 WHERE id = 1 | |
COMMIT | |
UPDATE account SET state = 'rich', money = money * 1000 WHERE id = 1 | |
COMMIT |
money가 10,000,000이 됩니다.
왜죠?
SQL 표준에서 isolation level은 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 네 가지입니다. SERIALIZABLE이 가장 높은 격리수준이지만 성능 상의 이유로 MySQL (InnoDB)은 REPEATABLE READ, PostgreSQL은 READ COMMITTED가 기본값입니다.
이러한 기본 isolation level에서 UPDATE
쿼리는 대상 레코드를 다른 트랜잭션이 먼저 업데이트한 뒤 커밋된 경우 업데이트 된 데이터를 보게 됩니다.
... a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the would-be updater will wait for the first updating transaction to commit or roll back (if it is still in progress). ... If the first updater commits, the second updater ... will attempt to apply its operation to the updated version of the row. (Postgres 문서)
The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. (MySQL 문서)