본문 바로가기

Back-End 공부/Database

[Database] 트랜잭션 격리 수준(isolation level) 문제점과 해결방법

📌 격리수준

Read Phenomena를 해결할 수 있는 여러가지 방법

LV1: Read-Uncommitted
  • 아직 커밋되지 않은 트랜잭션에서 처리중인 데이터를 다른 트랜잭션에서 읽는 것을 허용하는 격리 레벨
  • 정합성에 문제가 많은 격리수준이기 때문에 사용하지 않는 것을 권장한다.
    - Dirty Read, Non-Repeatable Read, Phantom Read 이상 현상이 모두 일어난다.
  • Commit이 되지 않는 상태지만 Update 된 값을 다른 트랜잭션에서 읽을 수 있다.
LV2: Read-Commited
  • RDB에서 대부분 기본적으로 사용되고 있는 격리수준
  • 다른 트랜잭션이 커밋된 데이터만 읽을 수 있기 때문에 Dirty Read와 같은 현상은 발생하지 않는다.
  • 실제 테이블 값을 가져오는 것이 아니라 Undo 영역에 백업된 레코드에서 값을 가져온다.
  Read-Committed 의 문제는?
  • 나의 트랜잭션에 여러 select 문이 있을 경우에, 그 사이에 다른 트랜잭션에서 update 또는 insert 등을 발생시키고 commit하게 될시 phantom read 또는 non-repeatable-read 발생이 가능하다.
LV3: Repeatable-Read
  • 데이터는 같은 트랜잭션 내에서는 항상 같은 값을 갖도록 하는 격리수준
  • 나의 트랜잭션에서 먼저 read하는 동안 다른 트랜잭션에서는 변경,추가 하더라도 같은 read값을 보장하는 것
  • 실제 테이블 값을 가져오는 것이 아니라 Undo 영역에 백업된 레코드에서 값을 가져온다.
  • InnoDB 스토리지 엔진은 트랜잭션이 Rollback 될 경우를 대비하여 변경 전 데이터를 UNDO에 백업해두고 실제 레코드 값을 변경하게 됨. 그리고 트랜잭션이 Commit을 하기 전에 다른 세션에서 해당 데이터를 조회 시 Undo를 참조하여 이전 값을 보여주는 것을 MVCC(Multi Version Concurrency Control) 라고 함

Repeatable-Read 의 문제는?

  • Phantom Read
    - 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상
    - 이를 방지하기 위해서는 쓰기 잠금을 걸어야 한다.
  • Repeatable Read를 하더라도 문제가 발생할 가능성 존재
    • 나의 트랜잭션이 read하는 동안 타 트랜잭션에서 update하게 되면 read해온 값이 달라지는 문제 발생
    • 대안은 locking
하지만 InnoDB에서는 독특한 특성 때문에 Repeatable Read 의 격리 수준에서도 Phantom Read 가 발생하지 않는다.
LV4: Serializable
  • 동시에 실행되는 여러 트랜잭션들을 순차적으로 실행한 것과 같은 결과를 보장하는 레벨
  • 데이터베이스 차원에서 동시에 특정 데이터에 접근하는 것을 차단한다.
    트랜잭션의 ACID 성질이 엄격하게 지켜지나 성능을 가장 떨어진다.

 

동시에 실행하는 것을 막을수록 (격리수준의 레벨이 높아질수록) 성능은 떨어진다

반대로, 격리수준의 레벨이 낮아질수록 데이터의 정합성이 낮아지니 잘 고려해서 선택해야 한다.

 

 

📌 Locking

💡 낙관적락 (Optimistic Lock)

  • 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것
  • DB에서 제공해주는 특징을 이용하는 것이 아닌 Application Level에서 잡아주는 Lock

도식도를 글로 나타내면 아래와 같습니다.

  1. A가 table의 Id 2번을 읽음 ( name = Karol, version = 1 )
  2. B가 table의 Id 2번을 읽음 ( name = Karol, version = 1 )
  3. B가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol2, version = 2 ) 성공
  4. A가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol1, version = 2 ) 실패
    • Id 2번은 이미 version이 2로 업데이트 되었기 때문에 A는 해당 row를 갱신하지 못함

 

위 flow를 통해서 같은 row에 대해서 각기 다른 2개의 수정 요청이 있었지만 1개가 업데이트 됨에 따라 version이 변경되었기 때문에 뒤의 수정 요청은 반영되지 않게 되었습니다. 이렇게 낙관적락은 version과 같은 별도의 컬럼을 추가하여 충돌적인 업데이트를 막습니다. version 뿐만 아니라 hashcode 또는 timestamp를 이용하기도 합니다.

낙관적 락은 version 등의 구분 컬럼을 이용해서 충돌을 예방합니다.

 

 

 

💡 비관적락 (Pessimistic Lock)

  • 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 걸고 시작하는 방법

 

✅ 비관적락 종류

 

- 공유락(shared lock - select for share)

  • Update에 대해서 Lock
  • 두 다른 트랜잭션이 동시에 Read하는 것은 가능 
  • Lost Update 문제 가능성 존재
  • ex) 상품주문의 최종 수량이 1개 -> transaction에 read && update가 있을때 -> 내 tran에서 1 read -> 타 트랜잭션이 1 read -> 내 tran에서 0으로 update -> 타 tran에서 0으로 update -> 최종 수량에 오류 발생

 

- 배타락(exclusive lock - select for update)

  • Update에 대해서 Lock, Read에도 Lock을 걸어 Lost Update 문제 해결
  • 해당 Row에 Lock을 걸어서 Update, Read모두 불가하게 만든다
  • Lost Update 문제 : 한 트랜잭션에서 데이터를 변경한 뒤 아직 커밋을 하지 않은 상태에서 읽어 들일 때, 다른 트랜잭션으로 인해 내가 작성한 변경사항이 덮어씌워지는 현상
  • 트래픽이 많지 않아, 동시성 이슈가 없다면 낙관적인 Lock 아니면 비관적인 Lock 사용

 

Shared Lock을 걸게 되면 write를 하기위해서는 Exclucive Lock을 얻어야하는데 Shared Lock이 다른 트랜잭션에 의해서 걸려 있으면 해당 Lock을 얻지 못해서 업데이트를 할 수 없습니다. 수정을 하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료(commit) 되어야합니다.

위의 도식도를 보면서 비관적 락에 대해서 제대로 이해해보도록 합니다.

  1. Transaction_1 에서 table의 Id 2번을 읽음 ( name = Karol )
  2. Transaction_2 에서 table의 Id 2번을 읽음 ( name = Karol )
  3. Transaction_2 에서 table의 Id 2번의 name을 Karol2로 변경 요청 ( name = Karol )
    • 하지만 Transaction 1에서 이미 shared Lock을 잡고 있기 때문에 Blocking
  4. Transaction_1 에서 트랜잭션 해제 (commit)
  5. Blocking 되어있었던 Transaction_2의 update 요청 정상 처리

이렇듯 Transaction을 이용하여 충돌을 예방하는 것이 바로 비관적 락(Pessimistic Lock)입니다.

 

 

 Repeatable Read 문제

Read -> [ 1️⃣ Update, Read ] -> Update -> [ 2️⃣ Update ]  -> Read

빨간색 : 내 트랜잭션

파란색 : 다른 트랜잭션

 

공유락 => 수정에 대해서 Lock

내가 Update하기 전에  다른트랜잭션이 1️⃣ 번 위치에서  Update할 때 못하게 막는 것. Read는 허용

-> 내 Update가 끝나고 2번 위치에서는 Update가능 -> 근본적 문제 해결 X

 

배타적락 => 수정, 읽기 모두 Lock

데이터는 안전해지지만 성능이 떨어짐

내 트랜잭션이 동작하는 동안 다른 트랜잭션 동작X -> 근본적 문제 해결

 

 

 Lock을 걸면 발생하는 문제

- DeadLock = 교착상태

  • 보통 배타적락에서 발생함
  • mairaDB는 교착상태가 지속되면 둘 다 ROLLBACK하는 전략 사용
  • author -> post 순서로만 접근하는 순방향 설계를 해야 한다. (post -> author까지 가능하면 데드락 이슈 발생하니까)

 

 

 실무적 해결책

위와 같은 동시성 이슈는 일반적이지는 않은 상황이지만,

쇼핑몰이벤트 또는 예매 시스템에서는 빈번하게 발생할수 있는 가능성 존재

  • Spring에서의 전략
    • optimistic lock
      • 버전정보 활용하여 update시에 정합성 체크
      • update item set stock_count = new_count where id=1 and version = 1; 만일, version이 맞지 않다면 0rows affected
    • pessimistic lock
      • 공유락
        • lock - PESSIMISTIC_READ
      • 배타락
        • lock - PESSIMISTIC_WRITE
        • 특정행에 대해 lock을 걸어 read조차 막음으로서 update시에 발생하는 이슈 원천 차단.
        • Serializable수준의 격리를 특정테이블과 특정데이터에 적용가능
  • queue사용
    • 이벤트, 예매 상황에서 고려될수 있는 아키텍처
    • 동시성에 대한 효율을 감수해야 하지만, 시스템의 정확도와 사용자경험의 향상을 꾀할수 있음
  • Redis 사용
    • 싱글스레드 기반 key-value 시스템

 

(+) 교착상태 정리하기

 

 

 

- references

https://seunghyunson.tistory.com/12

 

[ACID #3] Isolation이란? 트랜잭션의 격리 수준(isolation level)이란?

Isolation은 RDBMS를 정의하는 ACID 트랜잭션의 특성 중 I에 해당하는 특성입니다. 이번 포스팅에서 Isolation에 대해 설명하면서 다룰 내용들은 아래와 같습니다. Read Phenomena (읽기 이상 현상) Isolation Lev

seunghyunson.tistory.com

https://sabarada.tistory.com/175

 

[database] 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)

안녕하세요. 오늘은 낙관적 락과 비관적 락의 개념에 대해서 알아보는 시간을 가져보도록 하겠습니다. DB 충돌 상황을 개선할 수 있는 방법 database에 접근해서 데이터를 수정할 때 동시에 수정이

sabarada.tistory.com