프로그래밍 초식 : DB 트랜잭션 조금 이해하기 02 격리

Donghun Seol·2023년 4월 28일
0

프로그래밍 초식

목록 보기
10/13

레퍼런스

같은 데이터에 동시 접근

동시성은 초심자가 놓치기 쉬운 문제를 발생시킨다. 공유자원에 두 주체가 동시에 접근하고 각각 커밋할 때 의도하지 않은 동작이 발생할 수 있다. 특히 아래와 같이 교대로 읽기, 쓰기 작업이 일어날 때 동시성 문제가 발생하기 쉽다.

유저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 Committed

커밋된 데이터만 읽을 수 있다.

  • 커밋된 값과 트랜잭션 진행중인 값을 따로 보관하여
  • 트랜잭션 처리중인 값은 읽지 않는다.

커밋된 데이터만 덮어쓸 수 있다.

  • 행 단위 잠금을 사용한다.
  • 같은 데이터를 수정한 트랜잭션이 끝날 때 까지 대기한다.

읽는 동안 데이터 변경 1

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 

Repeatable Read

트랜잭션이 진행되는 동안에 데이터가 변경되더라도 같은 값을 읽게 해준다.
이 기능을 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 증가, 위키 페이지 수정등의 경우에 발생

변경 유실에 대한 대응 방법

원자적 연산

  • DB가 지원하는 원자적인 연산 사용.
  • 이 쿼리 대신 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
  • user1이 명시적으로 잠금한 대상에 질의하는 user2의 select는 user1의 커밋 이후에 실행된다.

CAS(Compare and Set)

  • 수정할 때 값이 같은지 비교해서 같은 경우에만 변경을 반영한다.
  • 수정 쿼리의 where절에 version까지 함께 비교한다.
  • 아래에서 쿼리 1이 먼저 수행되면 쿼리2는 수행되지 않는다.(버전 조건에서 필터됨)
    쿼리1: update wikipage set ver = 2, count = 2 where id = 1 and ver = 1
    쿼리2: update wikipage set ver = 2, count = 2 where id = 1 and ver = 1

읽는 동안 데이터 변경 2

한 트랜잭션의 결과가 다른 트랜잭션의 쿼리 결과에 영향을 주는 문제
두 트랜잭션이 각각 다른 대상에 쓰기 연산하지만 논리적으로는 경쟁상태가 되는 경우
당직자 문제의 예시

  • 최소 1인의 당직자는 존재해야함
  • 다음과 같은 트랜잭션이 발생하면 결과적으로 당직자가 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명이 되는 문제 발생

Serializable

인덱스 기반 잠금이나 조건 기반잠금을 사용해서
시리얼라이저블과 유사한 기능을 구현함

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명이 되는 문제 발생

정리

동시성은 초보자가 매우 놓치기 쉬운 문제

  • 동시성 문제와 격리 수준을 이해하면 파생되는 문제점을 줄일 수 있다.
  • DB단에서 이상한 문제가 생긴다면 동시성 문제를 의심해보자

잠금시간을 최소화

  • 잠금시간이 길어지면 성능(처리량)저하

동시성 문제를 다룰 때는 다음을 먼저 파악하자

  • 사용하는 DB의 기본 격리 레벨
  • DB 종류별 격리 레벨 동작 방식

초심자에게 어려운 내용이지만 매우 중요하다. 어렵지만 이해 했을 때 다른 초보자들과 차별화될 수 있는 토픽이다. 반복해서 학습하고 이해하자.

profile
I'm going from failure to failure without losing enthusiasm

3개의 댓글

comment-user-thumbnail
2023년 5월 12일

sample mocking comment for api test

답글 달기
comment-user-thumbnail
2023년 5월 13일

sample mocking comment for api test

답글 달기
comment-user-thumbnail
2023년 5월 13일

sample mocking comment for api test

답글 달기