본문 바로가기

Computer Science/DB

Lock을 이용한 동시성 제어 및 2PL(two phase locking)

안녕하세요. 오늘은 데이터베이스에서의 Lock에 대해 이야기하려 합니다. 여기서는 Lock의 종류, 호환성, 그리고 두 번째로도 Lock을 사용하면서 발생할 수 있는 이상현상에 대해 알아보겠습니다. 또한, 이러한 문제를 해결하기 위한 방법 중 하나인 2PL Lock에 대해서도 자세히 알아보겠습니다.

 

Lock의 종류

데이터베이스에서 Lock은 데이터의 동시 접근을 관리하고, 데이터의 일관성을 유지하기 위해 사용됩니다. 다음은 데이터베이스에서 사용하는 Lock의 주요한 두 가지 종류입니다.

Shared Lock (S-Lock, 혹은 Read Lock): 이 Lock은 데이터를 읽는 작업(read)에 사용됩니다. Shared Lock을 설정한 트랜잭션은 데이터를 읽을 수 있지만, 변경할 수는 없습니다. 변경을 할 수 없기 때문에 Read Lock이라고도 불립니다. 특징은 여러 트랜잭션이 동시에 같은 데이터에 Shared Lock을 설정할 수 있고 데이터를 동시에 읽어올 수 있다는 것입니다.

Exclusive Lock (X-Lock, 혹은 Write Lock): 이 Lock은 데이터를 변경하는 작업(write)에 사용됩니다. Exclusive Lock을 설정한 트랜잭션만 해당 데이터에 접근할 수 있습니다. 데이터에 대한 쓰기 작업을 할 수 있기 때문에 Write Lock이라고도 하는데요, 다만 Read를 못하는 것은 당연히 아닙니다. 중요한 점은 동시에 여러 트랜잭션이 같은 데이터에 Exclusive Lock을 설정할 수는 없고 배타적으로 작용한다는 것입니다.

 

Lock 호환성

Lock의 호환성이란 두 개의 Lock이 동시에 같은 데이터에 적용될 수 있는지를 의미합니다. Shared Lock과 Exclusive Lock의 경우, 두 가지의 Lock이 같은 데이터에 동시에 존재할 수 없습니다. 반면에 Shared Lock과 Shared Lock 사이에는 호환성이 있으므로, 동시에 여러 Shared Lock이 같은 데이터에 적용될 수 있습니다.

이를 표로 정리하면 아래와 같습니다.

그러나, 위처럼 배타적으로 Lock을 사용한다고 해서 이상 현상이 발생하지 않는 것은 아닙니다. 

 

이상 현상(Anomaly)

Shared Lock과 Exclusive Lock을 사용하더라도 여러 가지 이상현상이 발생할 수 있습니다. 대표적인 예로는 Dirty Read, Non-Repeatable Read, Phantom Read가 있습니다. 이는 이후의 MVCC에서 트랜잭션의 격리 수준에 따라 발생할 수 있는 이상현상이 달라지게 되고 DB에서 이러한 이상현상을 막기위한 해결책들이 있습니다만, 트랜잭션 격리 수준을 다룰 때 다시 한번 이야기해보겠습니다.

1. Dirty Read

트랜잭션 A가 X-Lock을 설정하고 데이터를 변경하였지만 아직 커밋하지 않았습니다. 이 때, 트랜잭션 B가 S-Lock을 설정하고 A가 변경한 데이터를 읽습니다. 만약 A가 롤백하면 B가 읽은 데이터는 더 이상 유효하지 않게 되는데, 이런 현상을 Dirty Read라고 합니다.



2. Non-Repeatable Read

트랜잭션 A가 Shared Lock을 설정하고 데이터를 읽습니다. 그런 다음, A가 다른 작업을 수행하는 동안 트랜잭션 B가 X-Lock을 설정하고 같은 데이터를 변경하고 커밋합니다. 이후 A가 다시 같은 데이터를 읽으면 처음에 읽은 데이터와 다른 결과가 나오게 됩니다. A는 동일한 트랜잭션 내에서 읽는 것만 반복했을 뿐인데 다른 데이터가 바뀐 셈이죠. 이를 다시 번복할 수 없는 Read라고 해서, Non-Repeatable Read라고 합니다.

 



3. Phantom Read

트랜잭션 A가 Shared Lock을 설정하고 데이터의 일정 범위를 읽습니다. 그런 다음, A가 다른 작업을 수행하는 동안 트랜잭션 B가 Exclusive Lock을 설정하고 A가 읽은 범위에 새로운 데이터를 추가하고 커밋합니다. 이후 A가 다시 같은 범위를 읽으면 처음에 읽지 않았던 새로운 데이터가 조회됩니다. 이를 Phantom Read라고 합니다. Non-repeatable Read와 비슷하지만 Insert라는 점이 다릅니다. 엄밀하게 말하면 Non-Repeatable Read에 포함되는 개념이라고 할 수 있는데, 굳이 나눠놓은 이유가 있습니다. 트랜잭션 격리 레벨에 따라서 발생가능한 Case가 달라지기 때문인데 마찬가지로 MVCC와 연결되는 개념입니다.

 



이러한 문제들은 읽기와 쓰기, 쓰기와 쓰기 Lock 간의 충돌로 인해 발생하며, 이런 현상을 방지하기 위한 여러 전략이 존재합니다. 이 중 하나가 2PL (2 Phase Locking)입니다.

 

2PL(2 Phase Locking)

위의 이상현상 3가지를 살펴보면, 공통적으로 한 트랜잭션 중간에 다른 트랜잭션이 개입하기 때문에 발생하는 현상입니다. 만약, Shared Lock을 unlock하기 전에 Exclusive Lock을 취득하면, 다른 트랜잭션이 개입할 수 없기 때문에 이상현상이 발생하지 않을 것입니다. 따라서 이러한 원리로, 2PL은 트랜잭션이 Lock을 획득하는 단계와 해제하는 단계를 명확하게 분리하는 방법입니다. 이 방법을 사용하면, 한 트랜잭션이 다른 트랜잭션의 중간 결과를 읽는 것을 방지할 수 있습니다. 

 

2PL은 아래 두 가지 Phase로 분류됩니다. 두 단계는 완전히 분리되며, 한번 unlock되기 시작하면 다시 lock을 획득하지 않기 때문에, 트랜잭션이 직렬화되는 것을 보장합니다.

 

1. Expanding Phase(growing Phase)

Lock을 취득하기만 하고 반환하지 않는 phase

 

2. Shrinking Phase (contracting Phase)

Lock을 반환만 하고 취득하지 않는 phase

 

참고로, 2PL을 사용하면 DB에서 데드락이 발생할 수 있습니다. 예를 들어, 상호간에 Shared Lock을 붙잡고 Exclusive Lock의 반환을 기다리는 상황이 생길 수 있는데 운영체제와 마찬가지로 데드락의 성립 조건을 충족시키지 않아야 합니다.

 

2PL의 종류

2PL에는 Conservative 2PL, Strict 2PL, 그리고 Strong Strict 2PL 세 가지 주요한 변형이 있습니다.

 

1. Conservative 2PL

  • 모든 lock을 취득한 뒤 transaction이 시작됩니다.
  • 필요한 모든 자원을 미리 취득하기 때문에 데드락의 발생 조건 중 non-preemptive를 만족시키지 못하기 때문에 데드락이 발생하지 않습니다.
  • 필요한 모든 자원을 취득한 뒤 트랜잭션이 시작되므로 대신 효율적이지 못합니다.

 

2. Strict 2PL(S2PL)

  • Strict schedule을 보장하는 2PL
  • recoverability 보장 - commit, rollback이 될 때까지 다른 트랜잭션이 끼어들지 못함
  • write-lock을 commit / rollback 될 때 반환

여기에서, Strict Schedule이란, 트랜잭션이 데이터에 대한 write lock (X-lock)을 획득한 경우, 해당 트랜잭션이 커밋하거나 롤백할 때까지 다른 트랜잭션은 그 데이터에 대해 read 또는 write를 수행할 수 없습니다.
다시 말해, 한 트랜잭션에 의해 변경된 데이터는 해당 트랜잭션이 완료될 때까지 다른 트랜잭션에 의해 접근되거나 변경될 수 없기 때문에, Dirty Read, Non-Repeatable Read, Phantom Read가 방지됩니다.

 

3. Strong strict 2PL(SS2PL)

  • strict schedule을 보장하는 2PL
  • Recoverability 보장
  • read-lock / write-lock 모두 commit/rollback될때 반환
  • S2PL보다 구현이 쉽다
  • Lock을 오래 쥐고 있기 때문에 효율이 떨어질 수 있다.

SS2PL은 트랜잭션의 모든 읽기 및 쓰기 작업에 대해 Lock을 획득하고, 트랜잭션이 커밋될 때까지 모든 Lock을 유지하여 Strict 2PL보다 더욱 엄격한 방식이지만 처리량이 떨어질 수 있으므로 상황에 따라 적절한 방법이 다를 수 있습니다.

 

여기까지 2PL에 대해서 살펴보았는데, Lock 호환성에 따라 작동하므로 트랜잭션 간 Read-Read를 제외하고는 한 쪽이 Block되므로 전체 처리량이 좋지 못하다는 단점이 있습니다. 이러한 문제를 해결하기 위해 등장한 개념이 MVCC입니다. MVCC는 트랜잭션에 대해 여러 버전을 유지하여 동시성을 향상시키는 방법입니다. 이는 다음 포스팅에서 다루어보겠습니다.