동시성은 초심자가 놓치기 쉬운 문제를 발생시킨다. 공유자원에 두 주체가 동시에 접근하고 각각 커밋할 때 의도하지 않은 동작이 발생할 수 있다. 특히 아래와 같이 교대로 읽기, 쓰기 작업이 일어날 때 동시성 문제가 발생하기 쉽다.
유저1 Read
유저2 Read
유저1 Write
유저2 Write
여러 클라이언트가 같은 데이터에 접근할 때 생기는 문제. 경쟁상태를 방지하기 위해 DBMS에서는 트랜잭션을 서로 격리한다. 격리하는 정도(=격리 수준)는 크게 4가지로 나뉜다.UCRRS
로 암기해보자.
일단, Read Uncommitted 격리수준은 실무에서 사용하지 않는다.
Read Uncommitted
Read Committed
Repeatable Read
Serializable
커밋되지 않은 데이터 읽기
커밋되지 않은 데이터 덮어쓰기
읽는 동안 데이터 변경 1
변경 유실
읽는 동안 데이터 변경 2
Dirty Read
let stock = [stock1]
let stockCount = 1
// below lines are pseudo code
user1 : insert into stock => void (stock = [stock1, stock2])
user2 : select * from stock => 2 // user1의 트랜잭션이 롤백된다면 커밋되지 않은 데이터를 읽은 셈
user2 : select cnt from stock => 1
user1 : update stockCount set 2 => void (stockCount = 2)
Dirty Write
// below lines are pseudo code
user1 : update asset set owner = 'A' where name = 'R1'
user2 : update asset set owner = 'B' where name = 'R1'
user2 : update asset set owner = 'B' where name = 'R2'
user1 : update asset set owner = 'A' where name = 'R2'
user1과 user2가 asset의 owner를 각각 교차해서 쓰는 경우
user1은R1 = A, R2 = A
를 기대하고
user2는R1 = B, R2 = B
를 기대하지만
실제는 R1 = B, R2 = A
가 되었다.
Dirty Read, Write 모두 커밋되지 않은 데이터에 덮어쓰기를 허용해서 생기는 문제.
커밋된 데이터만 읽을 수 있다.
커밋된 데이터만 덮어쓸 수 있다.
Read Skew
읽는 시점에 따라 데이터가 변경됨.
user1은 A를 읽고 나서 B를 읽는다.
user1이 B를 읽기 직전에 user2는 A, B를 모두 변경하고 커밋한다.
user1이 B를 읽었을 때 데이터 무결성이 깨짐
// below lines are pseudo code
// A + B MUST BE 20
A = 10
B = 10
user1 : select pnt from points where id = 'A' => 10
user2 : update points set pnt += 1 where id = 'A'
user2 : update points set pnt -= 1 where id = 'B'
user2 : COMMIT!!
user1 : select pnt from points where id = 'B' => 9 // Error A + B != 20
트랜잭션이 진행되는 동안에 데이터가 변경되더라도 같은 값을 읽게 해준다.
이 기능을 MVCC(Multi Version Concurrency Control)로 구현한다. 읽는 시점에 특정 버전에 해당하는 데이터만 읽을 수 있다.
// below lines are pseudo code
// A + B MUST BE 20
A(v1) = 10
B(v1) = 10
user1 : select pnt from points where id = 'A' => 10
user2 : update points set pnt += 1 where id = 'A' => A(v2) = 11 // v2를 생성하고 쓴다.
user2 : update points set pnt -= 1 where id = 'B' => B(v2) = 9 // v2의 값을 바꾼다.
user2 : COMMIT!!
user1 : select pnt from points where id = 'B' => 10 // user1은 B(v1)을 읽는다.
Lost Update
같은 데이터를 쓸 때 발생.
count 증가, 위키 페이지 수정등의 경우에 발생
원자적 연산
update article set readcnt = 2 where id = 1
update article set readcnt = readcnt + 1 where id = 1
명시적 잠금
select * from wikipage where id = 1 for update
CAS(Compare and Set)
update wikipage set ver = 2, count = 2 where id = 1 and ver = 1
update wikipage set ver = 2, count = 2 where id = 1 and ver = 1
한 트랜잭션의 결과가 다른 트랜잭션의 쿼리 결과에 영향을 주는 문제
두 트랜잭션이 각각 다른 대상에 쓰기 연산하지만 논리적으로는 경쟁상태가 되는 경우
당직자 문제의 예시
worker1 : select count(*) from oncall where today and duty = true => 2
worker2 : select count(*) from oncall where today and duty = true => 2
worker1 : update oncall set duty=false where today and name='worker1'
worker2 : update oncall set duty=false where today and name='worker2'
worker1 : commit
worker2 : commit
// 최종적으로 당직자가 0명이 되는 문제 발생
인덱스 기반 잠금이나 조건 기반잠금을 사용해서
시리얼라이저블과 유사한 기능을 구현함
worker1 : select count(*) from oncall where today and duty = true => 2
worker2 : select count(*) from oncall where today and duty = true => 2
worker1 : update oncall set duty=false where today and name='worker1' // 🔒︎ 획득
worker2 : update oncall set duty=false where today and name='worker2' // ❌ 실행 불가
worker1 : commit
worker2 : commit // 당연히 커밋 불가
// 최종적으로 당직자가 0명이 되는 문제 발생
동시성은 초보자가 매우 놓치기 쉬운 문제
잠금시간을 최소화
동시성 문제를 다룰 때는 다음을 먼저 파악하자
초심자에게 어려운 내용이지만 매우 중요하다. 어렵지만 이해 했을 때 다른 초보자들과 차별화될 수 있는 토픽이다. 반복해서 학습하고 이해하자.
sample mocking comment for api test