여러 트랜잭션이 동시에 데이터를 변경하는 과정에서, 한 트랜잭션의 수정 내용이 다른 트랜잭션의 수정 내용에 의해 덮어씌워져 최종적으로 의도와 다른 값이 저장되는 현상을 말한다.

이번 글에서는 MySQL, PostgreSQL, MSSQL에서 Lost Update가 발생하는 상황과, 이를 방지하는 방법에 대해 살펴본다. (낙관적 락은 제외)
MySQL InnoDB의 REPEATABLE READ는 MVCC 기반이므로, 기본적으로 읽기 시점에 해당 레코드의 스냅샷 버전을 고정해서 봅니다.
이로 인해 서로 다른 트랜잭션이 같은 레코드를 읽고, 각자 수정한 후 커밋하면, 마지막에 커밋한 트랜잭션이 이전 변경을 덮어써버립니다.
같은 MVCC 기반이라도, MySQL과 PostgreSQL은 REPEATABLE READ의 쓰기 충돌 처리 방식이 다릅니다.
PostgreSQL REPEATABLE READ
could not serialize access 오류로 롤백MySQL REPEATABLE READ
이 차이 때문에, PostgreSQL은 모든 트랜잭션이 REPEATABLE READ 이상이면 Lost Update를 막지만,
MySQL은 SELECT ... FOR UPDATE 또는 SERIALIZABLE로 격리수준을 높이지 않으면 Lost Update가 발생할 수 있습니다.
T1: BEGIN TRANSACTION (RR)
T2: BEGIN TRANSACTION (RR)
T1: SELECT balance → 100 (스냅샷)
T2: SELECT balance → 100 (스냅샷)
T2: UPDATE balance = 90
T2: COMMIT
T1: UPDATE balance = 80
→ "네가 본 balance=100은 이미 변경됨" 감지
→ ERROR: could not serialize access
T1: ROLLBACK
T1: BEGIN TRANSACTION (RR)
T2: BEGIN TRANSACTION (RR)
T1: SELECT balance → 100 (스냅샷)
T2: SELECT balance → 100 (스냅샷)
T2: UPDATE balance = 90
T2: COMMIT
T1: UPDATE balance = 80
→ 현재 레코드(balance=90) 덮어씀
T1: COMMIT
최종 balance = 80 (T2 변경 손실)
Locking Read 사용
SELECT ... FOR UPDATE
SERIALIZABLE 격리수준
PostgreSQL의 REPEATABLE READ는 MVCC 기반이지만, 쓰기 시점에 버전 충돌 감지 기능이 있습니다.
Serialization Failure 또는 could not serialize access 오류를 발생시킴 → 롤백됨중요: 모든 트랜잭션이 REPEATABLE READ 이상이어야 함.
한쪽이 READ COMMITTED라면 충돌 감지가 안 되고 Lost Update 발생 가능.
SELECT ... FOR UPDATE로 명시적 잠금MSSQL은 MySQL, PostgreSQL과 달리 MVCC가 아닌 Lock 기반 동시성 제어를 기본으로 사용합니다.
WITH (UPDLOCK, HOLDLOCK) 같은 비관적 잠금 사용 권장주의: MSSQL에서 Lost Update가 다시 가능해지는 경우
MSSQL은 기본적으로 Lock 기반 동시성 제어를 사용하며, 모든 트랜잭션이 REPEATABLE READ 이상이면 Lost Update가 발생하지 않습니다.
하지만 다음과 같은 환경에서는 Lost Update 가능성이 존재합니다.
READ_COMMITTED_SNAPSHOT = ON 활성화 시 (RCSI 켜짐)
- READ COMMITTED 격리 수준이 MVCC(버전 기반) 방식으로 동작합니다.
- 이 설정은 READ COMMITTED 트랜잭션에만 영향을 주며,
REPEATABLE READ 트랜잭션은 여전히 잠금(S Lock)을 유지합니다.- 그러나, 시스템에 REPEATABLE READ와 READ COMMITTED(RCSI) 트랜잭션이 혼재하는 경우,
READ COMMITTED(RCSI) 트랜잭션은 S Lock을 오래 유지하지 않으므로,
REPEATABLE READ 쪽의 보호가 깨지고 Lost Update가 발생할 수 있습니다.ALLOW_SNAPSHOT_ISOLATION = ON 활성화 및 SNAPSHOT 격리 사용 시
- SNAPSHOT 격리 트랜잭션은 S Lock을 사용하지 않고 버전 데이터를 읽습니다.
- 따라서 REPEATABLE READ 트랜잭션과 혼합되면 동시 갱신이 가능하며,
충돌이 발생하면 SNAPSHOT 트랜잭션은 오류(에러 3960)로 롤백되지만,
READ COMMITTED(RCSI) 트랜잭션과 혼합될 경우에는 Lost Update가 발생할 수 있습니다.혼합 격리 수준 환경
- 한쪽이 REPEATABLE READ, 다른 쪽이 READ COMMITTED(RCSI) 또는 SNAPSHOT 격리인 경우
- REPEATABLE READ가 기대하는 S Lock 기반 보호가 깨지므로 Lost Update 위험이 있습니다.
- 이를 방지하기 위해, 충돌 가능 구간에서
WITH (UPDLOCK, HOLDLOCK)등 명시적 잠금을 사용하는 것이 안전합니다.
| DB / 격리수준 | Lost Update 가능 여부 | 방지 방식 |
|---|---|---|
| MySQL RR | 🔺 가능 | MVCC 스냅샷 기반 → FOR UPDATE 필요 |
| MySQL SERIALIZABLE | ⭕ 불가능 | 모든 읽기 공유락 + Next-Key Lock |
| PostgreSQL RR | ⭕ 불가능(모두 RR 이상일 때) | 버전 충돌 감지 후 롤백 |
| PostgreSQL SERIALIZABLE | ⭕ 불가능 | SSI 기반 직렬성 위반 감지 |
| MSSQL RR | ⭕ 불가능(모두 RR일 때) | S Lock 유지 + UPDATE 시 최신 버전 재확인 |
| MSSQL SERIALIZABLE | ⭕ 불가능 | RR 방식 + Key-Range Lock |