DB

Database Lock, Isolation Level

shmallow 2023. 4. 27. 10:19

Lock ? 

Lock은 다중 프로세스나 스레드에서 공유 자원에 대한 접근을 제어하기 위해 사용되는 동기화 기술을 말한다. 데이터의 무결성과 일관성을 지키기 위해 Lock을 사용한다. Database Lock은 크게 공유 락과 베타 락으로 나뉜다. 

 

공유락 : 데이터를 읽을 때 사용하는 락으로, 다른 트랜잭션이 해당 데이터를 읽을 수 있도록 허락한다. 즉, 공유 락이 걸린 데이터를 다른 트랜잭션도 읽을 수 있지만, 하나의 트랜잭션이 쓰기 작업을 수행하려면 공유 락이 끝날 때 까지 기다려야 한다. 따라서 공유락을 사용하여 여러 개의 트랜잭션이 동시에 데이터를 변경하는 것을 방지한다.

베타 락 : 일반적으로 쓰기 작업이 필요한 경우 사용되며, 해당 락을 획득한 트랜잭션만이 데이터를 변경할 수 있다. 배타적 락이 걸려 있는 데이터는 다른 트랜잭션에서 읽거나 쓰기 작업을 수행할 수 없다. 따라서 다른 트랜잭션에서 해당 데이터가 접근하는 것을 방지하여 데이터의 일관성을 유지한다.

하지만 Lock을 사용하더라도 이상 현상이 발생할 수 있는데, 아래와 같은 예제를 살펴보자. 현재 x = 100, y = 200의 데이터가 저장되어 있고, 트랜잭션 A는 x + y의 합을 y에 저장하고, 트랜잭션 B는 x + y의 합을 x에 저장한다고 했을 때,

  1. 트랜잭션 B가 x 데이터를 읽기 위해 공유락(x)을 사용하고 데이터를 읽어오고 공유락(x)을 해제한다. (x = 100)
  2. 트랜잭션 A가 y 데이터를 읽기 위해 공유락(y)을 사용한다.
  3. 트랜잭션 B가 y 데이터에 쓰기 위해 베타락(y)을 사용하려고 하는데, 공유락(y)이 있어서 blocking 된다.
  4. 트랜잭션 A가 y 데이터를 읽은 다음, 공유락(y)을 해제한다. (y = 200)
  5. 트랜잭션 B가 베타락(y)을 사용해 y 데이터를 읽고(y = 200), y를 300으로 업데이트 한 뒤 베타락(y)을 해제한다.
  6. 트랜잭션 A가 베타락(x)을 사용해 x 데이터를 읽고(x = 100), x를 300으로 업데이트 한 뒤 베타락(x)을 해제한다.

위 결과에서 x, y 데이터는 각각 300이 들어가있게 된다. 하지만 트랜잭션 B -> 트랜잭션 A 순서로 되게 한다면 x = 400, y = 300이 들어가 있게 된다. 따라서 위 스케줄은 Nonserializable한 스케줄로 트랜잭션 간에 일관성을 유지할 수 없어서 여러가지 문제(Dirty read 등)가 발생할 수 있다. 따라서 이 문제를 해결하기 위해 2PL Protocol이 등장했다.

2PL Protocol

2PL Protocol은 여러 트랜잭션들이 동시에 데이터에 접근하는 것을 제어하는 방법 중 하나로 데이터 불일치를 방지하고 여러 트랜잭션이 동시에 데이터를 수행하더라도 수행 결과가 일관성이 있도록 보장하게 해준다. 

Deadlock

2PL Protocol을 적용하더라도 발생할 수 있는 문제가 있는데, 그게 바로 Deadlock이다. Deadlock이란 서로 다른 트랜잭션 작업이 서로 상대방의 작업이 끝나기만을 기다리고 있어 무한 대기 상태에 빠진 상황을 말한다. 데드락은 주로 아래 네 가지 조건이 동시에 충족되었을 때 발생한다.

  • 상호 배제 : 한 번에 하나의 트랜잭션만 해당 자원(테이블 또는 행)을 사용할 수 있다.
  • 점유와 대기 : 트랜잭션은 이미 어떤 자원을 점유하고 있는 상태에서 다른 자원을 기다리고 있는 상황이다.
  • 비선점 : 한 트랜잭션이 점유한 자원을 다른 트랜잭션에서 강제로 뺏을 수 없다.
  • 순환 대기 : 트랜잭션들이 서로 다른 순서로 자원에 액세스하려고 할 때 순환적인 대기 상황이 발생할 수 있다.

MVCC (Multi-Version Concurrency Control)

MVCC란 데이터베이스에서 동시성 제어를 위한 기술로, 각 트랜잭션에 대해 데이터의 여러 버전을 유지하므로, 여러 트랜잭션이 동시에 데이터에 액세스할 수 있다. 이로 인해 동시성이 높아지고, 대기시간이 줄어들어서 더 나은 성능을 제공한다. MVCC의 주요 원칙은 다음과 같다.

1. 격리 : 트랜잭션 격리 수준을 효율적으로 제공한다. 트랜잭션은 자신이 시작한 시점의 데이터 스냅샷에 대해 작업을 수행하며, 다른 트랜잭션의 변경 사항에 영향을 받지 않는다.

2. 일관성 : 트랜잭션 중간에 다른 트랜잭션이 데이터를 변경하더라도 해당 트랜잭션은 자신의 스냅샷에 대해서만 작업을 계속 수행한다.

3. 논블로킹 : 트랜잭션 간에 대부분의 경우 락을 사용하지 않아서 블로킹될 경우가 적어지며 더 나은 성능과 동시성을 제공한다.

MVCC는 Isolation level에 따라 트랜잭션 간의 데이터 공유 정도가 달라지므로 서로 조합하여 성능과 일관성 사이의 균형을 찾는게 좋다.

 

Lock의 설정 범위

1. 데이터베이스 : 전체 데이터베이스를 기준으로 lock을 건다. 즉, 1개의 세션만이 DB 데이터에 접근이 가능하다. 주로 DB 소프트웨어 버전을 업데이트할 때 사용

2. 파일 : 데이터베이스 파일을 기준으로 lock을 설정한다. 파일이란 실제 데이터가 쓰여지는 물리적인 저장소를 말한다.

3. 테이블 : 테이블 기준으로 lock을 설정한다. 테이블의 모든 행을 업데이트하는 등의 전체 테이블에 영향을 주는 변경을 수행할 때 유용하다.

4. 페이지와 블럭 : 파일의 일부인 페이지와 블록을 기준으로 Lock을 설정한다.

5. 행 : 1개의 행을 기준으로 Lock을 설정한다. 일반적으로 사용하는 Lock 설정 범위


Isolation Level

격리 수준(isolation level)이란 DBMS에서 동시에 실행되는 여러 트랜잭션들 사이의 상호 작용을 제어하는 방법을 말한다. 격리 수준은 데이터의 일관성과 동시성 간의 균형을 조절하는 데 사용되며, 트랜잭션 처리에서 발생할 수 있는 일부 문제를 방지하거나 최소화하는데 도움이 된다. 트랜잭션을 처리하는데 발생할 수 있는 문제는 크게 아래와 같이 존재한다.

 

1. dirty read

한 트랜잭션에서 아직 커밋하지 않은 데이터를 다른 트래잭션에서 읽는 현상을 의미한다. 예를들어 트랜잭션 A, B가 동시에 실행된다고 했을 때, 

  1. 트랜잭션 A가 시작된다. (abc계좌 입금 트랙잭션)
  2. 트랜잭션 A에서 abc계좌 잔액에 1000원을 추가
  3. 트랜잭션 B가 시작된다. (abc계좌 잔액 확인 트랜잭션)
  4. 트랜잭션 B는 계좌 잔액을 읽는다. 여리것 잔액에는 트랜잭션 A가 입금한 1000원이 포함되어 있다.
  5. 트랜잭션 A에서 문제가 발생해 롤백이 수행된다. 고객 A의 입금이 취소된다.
  6. 트랜잭션 B는 잘못된 잔액 정보를 사용하여 작업을 계속 진행한다.

위처럼 아직 커밋되지 않은 트랜잭션 A의 데이터를 읽음으로써 발생하는 현상이 Dirty Read 현상이다.

 

2. nonrepeatable read

하나의 트랜잭션 내에서 같은 데이터를 두 번 이상 읽을 때 일관성 없는 결과를 얻는 현상을 말한다. 주로 한 트랜잭션 도중에 다른 트랜잭션이 해당 데이터를 수정하고 커밋하는 경우에 발생한다. 

  1. 트랜잭션 A가 시작된다. (abc계좌 출금 트랜잭션)
  2. 트랜잭션 A에서 abc계좌 잔액을 읽는다. 현재 잔액은 2000원이다.
  3. 트랜잭션 B가 시작된다. (abc 계좌 입금 트랜잭션)
  4. 트랜잭션 B에서 계좌 잔액에 1000원을 추가한다. 새로운 잔액은 3000원이다.
  5. 트랜잭션 B가 커밋되고 완료된다.
  6. 트랜잭션 A는 다시 계좌 잔액을 읽는다. 이번에는 3000원으로, 이전과 다른 값을 얻는다.
  7. 트랜잭션 A는 출금 처리를 완료한다.

위 예시에서 트랜잭션 A 내에서 두 번째 계좌 잔액 조회 시 일관성 없는 결과를 얻게되었다. 이는 트랜잭션 isolation 관점에 위반되는 행위다.

 

3. phantom read

하나의 트랜잭션 내에서 두 번 이상 같은 쿼리를 실행할 때 일관성 없는 행 수를 얻는 현상을 말한다. 이 문제는 한 트랜잭션 도중에 다른 트랜잭션이 새로운 데이터를 삽입하거나 삭제하고 커밋하는 경우에 발생한다.

  1. 트랜잭션 A가 시작된다. (계좌 조회 트랜잭션)
  2. 트랜잭션 A에서 1000원 이상인 계좌를 조회한다. 현재 결과는 3개의 계좌가 보여진다.
  3. 트랜잭션 B가 시작된다. (입금 트랜잭션)
  4. 트랜잭션 B에서 계좌에 1000원을 입금하는데, 해당 계좌는 0원에서 1000원이 된다.
  5. 트랜잭션 B가 커밋되고 완료된다.
  6. 트랜잭션 A는 다시 잔액이 1000원 이상인 계좌를 조회한다. 이번에는 결과가 4개의 계좌로 변경되었다.
  7. 트랜잭션 A가 완료된다.  

위 예시에서 트랜잭션 A 내에서 두 번째 계좌 조회 시 일관성 없는 결과를 얻게되는데 이 현상을 phantom read 현상이라고 한다.

 

 위처럼 이상 현상들이 모두 발생하지 않게 만들 수는 있지만 그러면 제약사항이 많아지므로 동시 처리 가능한 트랜잭션 수가 줄어들어 결국 DB의 전체 처리량이 하락하게 되어 DB성능이 안좋아진다. 따라서 일부 이상현상은 허용하는 몇 가지 level을 만들어 사용자가 필요에 따라서 적절하게 선택할 수 있도록 하는게 바로 isolation level이다. 표준 SQL 기준으로 isolation level은 아래와 같이 존재한다.

 

1. READ UNCOMMITTED (읽기 미완료) : 가장 낮은 격리 수준으로, 다른 트랜잭션에서 아직 커밋되지 않은 데이터도 읽을 수 있게 한다. 동시성이 높고 성능이 좋지만, 데이터 일관성에 문제가 발생할 수 있다.

2. READ COMMITTED (읽기 완료, 오라클 기본 격리 수준) : 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있는 격리 수준이다. 일반적으로 많은 DBMS에서 기본적으로 사용되는 격리 수준이다.

3. REPEATABLE READ (반복 읽기) : 트랜잭션 내에서 같은 데이터를 여러 번 읽어도 일관된 결과를 얻을 수 있는 격리 수준이다. 이 격리 수준에서는 한 트랜잭션이 데이터를 읽을 때 해당 데이터에 대한 공유 잠금이 설정되며, 다른 트랜잭션에서 해당 데이터를 수정할 수 없다.

4. SERIALIZABLE (직렬화 가능) : 가장 높은 격리 수준으로, 트랜잭션들이 순차적으로 실행되는 것처럼 보인다. 이 격리 수준에서는 한 트랜잭션이 데이터를 읽을 때 범위 잠금이 설정되어, 다른 트랜잭션이 해당 범위의 데이터를 수정하거나 삽입할 수 없다. 따라서 데이터의 일관성이 높아지는 장점이 있지만 동시성이 낮고 성능에 부담이 될 수 있다.

각 격리 수준은 성능과 데이터 일관성 간의 트레이드오프를 고려해서 선택해야 한다. mysql 에서는 위 4가지 isolation level을 제공하지만 오라클은 read committed, serializable level만 제공한다. 오라클은 추가적으로 serializable level은 snapshot isolation level을 사용하고 있다.

 

5. SNAPSHOT ISOLATION LEVEL : 트랜잭션에서 읽기 작업을 수행할 때 일관된 snapshot을 사용하여 동시성 문제를 해결하는 방법으로, 다중 버전 동시성 제어(MVCC) 메커니즘을 사용한다. 즉 트랜잭션 시작 시점에 데이터베이스에서 일관된 snapshot을 생성하고, 해당 snapshot을 기반으로 모든 읽기 작업을 수행한다. 이렇게 함으로써 다른 트랜잭션에서 해당 데이터를 수정하더라도 트랜잭션에서 사용하는 데이터의 일관성을 보장할 수 있다. 또 서로 다른 트랜잭션에서 같은 데이터를 write, write 충돌 발생시 먼저 커밋한 트랜잭션을 적용하고, 그 이후 트랜잭션은 rollback되는 특징이 있다.

 

위에서는 이상 현상으로 3가지만 소개되었지만 실제로는 더 다양한 이상 현상이 존재한다. 또 다른 몇 가지를 알아보자.

4. dirty write

한 트랜잭션에서 수행한 데이터가 아직 커밋되지 않았는데, 다른 트랜잭션이 그 데이터를 덮어쓰는 현상을 말한다. 

  1. 트랜잭션 A가 시작된다. (abc 계좌 인출)
  2. 트랜잭션 A가 abc 계좌 잔액을 900원으로 업데이트하고 있지만, 아직 커밋은 하지 않았다.
  3. 트랜잭션 B가 시작된다. (abc 계좌 입금)
  4. 트랜잭션 B가 abc 계좌 잔액을 1000원으로 업데이트하고 커밋한다.
  5. 트랜잭션 A가 rollback되면 트랜잭션 B에서 커밋한 1000원으로 남게된다.

이렇게 dirty write는 트랜잭션 A의 변경 사항이 트랜잭션 B에 의해 덮어쓰여지는 현상을 말한다. 이 현상이 발생하면 데이터의 복구가 어려울 수 있으므로 모든 isolation level에서 dirty write를 방지하기 위해 베타 락을 사용한다.

 

5. lost update

두 개 이상의 트랜잭션이 동시에 같은 데이터를 수정하려고 할 때 발생하는 현상으로 한 트랜잭션의 변경사항이 다른 트랜잭션에 의해 덮어쓰여 손실되는 현상을 말한다.

  1. 트랜잭션 A가 시작된다. (abc 계좌 인출)
  2. 트랜잭션 A가 abc 계좌 잔액을 900원으로 업데이트하고 있지만, 아직 커밋은 하지 않았다.
  3. 트랜잭션 B가 시작된다. (abc 계좌 입금)
  4. 트랜잭션 B가 abc 계좌 잔액을 1100원으로 업데이트하고 커밋한다.
  5. 트랜잭션 A가 커밋을 수행하면, 트랜잭션 B의 변경사항이 손실되고, abc 계좌 잔액은 900원이 된다.

dirty write와 비슷하지만, dirty write는 아직 커밋되지 않은 변경 사항이 덮어쓰여지는것에 초점을 맞추고, lost update는 두 트랜잭션 간의 변경 사항 충돌로 인해 한 트랜잭션의 변경 사항이 손실되는 것에 초점을 맞춘다.

추가

1. 오라클에서 MVCC를 적용했을 때, 같은 데이터를 두 개의 트랜잭션에서 변경할 경우

  1. 트랜잭션 1이 데이터를 읽고 수정하면 해당 데이터는 트랜잭션 1에 의해 잠기게 되며, 트랜잭션 1이 커밋하거나 롤백하기 전까지 다른 트랜잭션에서 접근할 수 없습니다.
  2. 트랜잭션 2가 동일한 데이터를 수정하려고 하면, 트랜잭션 1이 락을 해제할 때까지 트랜잭션 2는 대기 상태에 놓이게 됩니다.
  3. 트랜잭션 1이 커밋하면, 데이터는 트랜잭션 1에 의한 변경 내용으로 갱신되고 해당 데이터에 대한 락이 해제됩니다.
  4. 이후 트랜잭션 2는 대기 상태에서 벗어나 해당 데이터를 수정할 수 있게 됩니다. 트랜잭션 2가 커밋하면 데이터는 트랜잭션 2에 의한 변경 내용으로 다시 갱신됩니다.

따라서, 최종적으로 데이터는 트랜잭션 2의 변경 내용으로 갱신되게 됩니다. 이러한 동시성 제어 메커니즘은 데이터의 일관성을 보장하고 동시 수정으로 인한 데이터 충돌을 방지하는 데 중요한 역할을 합니다.

 

 

 

참고

https://blog.naver.com/PostView.naver?blogId=sdug12051205&logNo=221575076036&categoryNo=44&parentCategoryNo=0&viewDate=&currentPage=1&postListTopCurrentPage=1&from=postView 

 

2 Phase Locking (2PL)

q  2PL(2 Phase Locking) 정의 -     기본 로킹 규약의 문제를 해결하고 트...

blog.naver.com

MVCC 배경

https://medium.com/monday-9-pm/mvcc-multi-version-concurrency-control-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-e4102cd97e59

 

'DB' 카테고리의 다른 글

Oracle RAC (Real Application Clusters)  (1) 2023.05.24
DBCP (Database Connection Pool)  (0) 2023.05.17
인덱스 스캔 종류  (0) 2023.04.12
Oracle 무작위 학습  (0) 2022.12.06
오라클 에러 메시지 모음  (0) 2022.08.28