잠금(Locking)은 하나의 트랜잭션이 실행하는 동안 특정 데이터 항목에 대해서 다른 트랜잭션이 동시에 접근하지 못하도록 상호배제(Mutual Exclusive) 기능을 제공하는 기법이다.
하나의 트랜잭션이 데이터 항목에 대하여 잠금(lock)을 설정하면, 잠금을 설정한 트랜잭션이 해제(unlock)할 때까지 데이터를 독점적으로 사용할 수 있다.
잠금 기법은 기본적으로 lock 연산과 unlock 연산을 사용한다.
잠금 연산은 데이터에 대한 연산의 성격에 따라 공유잠금(Shared lock: S-lock)과 배타잠금(Exclusive lock: X-lock)으로 나눌 수 있다.
모든 트랜잭션은 데이터 항목에 대한 접근을 시도할 때 둘 중 하나의 잠금 방법을 사용한다.
공유잠금과 배타잠금 모두 잠금을 해제할 경우에는 unlock 연산을 이용한다.
S-lock(공유잠금)
• 공유잠금을 설정한 트랜잭션은 데이터 항목에 대해 읽기 연산(read)만 가능하다.
ex) T1에서 x에 대해 S-lock을 설정했다면, T1은 read(x) 연산만 가능하다.
• 하나의 데이터 항목에 대해 여러 개의 공유잠금이(S-lock) 가능하다.
ex) T1에서 x에 대해 S-lock을 설정한 경우, 동시에 T2에서도 x에 대해 S-lock을 설정할 수 있다.
• 다른 트랜잭션도 읽기 연산(read) 만을 실행할 수 있다.
ex) T1에서 x에 대해 S-lock을 설정했다면, T2에서도 T1이 S-lock(x)을 실행하는 동안 read(x) 연산만 가능하다.
X-lock(배타잠금)
• 배타잠금을 설정한 트랜잭션은 데이터 항목에 대해서 읽기 연산(read)과 쓰기 연산(write) 모두 가능하다.
ex) T1에서 x에 대해 S-lock을 설정했다면, T1은 read(x) 연산과 write(x) 연산 모두 가능하다.
• 하나의 데이터 항목에 대해서는 하나의 배타잠금(X-lock)만 가능하다.
동시에 여러 개의 배타잠금은 불가능하다.
ex) T1에서 x에 대해 X-lock을 설정했다면, T1에서 unlock(x)를 하기 전까지 T2에서 x에 대해 X-lock을 설정할 수 없다.
• 다른 트랜잭션은 읽기 연산(read)와 쓰기 연산(write) 모두 불가능하다.
ex) T1에서 x에 대해 X-lock을 설정했다면, T2에서는 T1에서 unlock(x)를 하기 전까지 read(x), write(x) 연산이 모두 불가능하다.
• True는 동시 잠금이 가능함, False는 동시 잠금이 가능하지 않음을 의미한다.
• S-lock만 동시 잠금이 가능하다.
하나의 데이터 항목에 대해서 동시에 두 개 이상의 S-lock 설정이 가능하다는 의미이다.
잠금 설정 규칙
• 트랜잭션은 데이터 항목 x에 대해 read(x) 연산을 실행하기 전에 S-lock(x)이나 X-lock(x) 중 하나를 실행해야 한다.
• write(x) 연산을 실행하기 위해서는 X-lock(x)을 실행해야 한다.
• 연산 종료 후에는 unlock(x) 연산을 실행해야 한다.
• S-lock(x)이나 X-lock(x) 연산 실행 후에만 unlock(x) 연산을 실행할 수 있다.
여러 트랜잭션이 하나의 데이터를 동시에 읽을 때(read 연산)는 문제가 되지 않는다.
그러나 어느 한 쪽이라도 쓰는 경우(write 연산)에는 트랜잭션 간의 상호 간섭으로 인해 잘못된 결과가 발생할 수 있다.
그러므로 읽기 연산(read)을 실행하기 위해 설정하는 S-lock은 여러 트랜잭션에서 동시에 사용 가능하지만, 쓰기 연산(write)을 실행하기 위해 설정하는 X-lock은 동시에 사용할 수 없다고 이해하면 된다.
잠금 단위(Locking Granularity)
잠금 단위는 잠금의 대상이 되는 데이터 객체의 크기를 의미한다.
작게는 레코드의 필드 값, 하나의 레코드, 물리적 입출력 단위가 되는 디스크 블록이 될 수도 있으며, 크게는 테이블이나 데이터베이스까지 하나의 잠금 단위가 될 수 있다.
잠금 단위가 클수록 동시성(병행성) 수준은 낮아지고, 동시성 제어 기법은 간단해진다.
잠금 단위가 작을수록 동시성(병행성) 수준은 높아지고, 관리는 복잡해진다.
ex) 잠금 단위가 레코드일 경우 VS 테이블일 경우
• 잠금 단위 = 레코드 : 두 개의 트랜잭션이 하나의 테이블에 대해 연산을 수행하더라도, 접근하는 레코드가 서로 다르면 동시에 실행 가능하다. 그러나 접근하는 레코드마다 일일이 잠금을 설정해야 하므로 관리는 복잡해진다.
• 잠금단위 = 테이블 : 동시에 실행이 불가능하지만, 테이블 하나에만 잠금 설정이 필요하므로 관리가 쉽다.
따라서 잠금 단위를 여러 단계로 정해 놓고 필요에 따라 혼용하는 방식이 많이 사용된다.
잠금의 한계
잠금은 대부분의 DBMS에서 사용되는 방식이지만 다음과 같은 한계가 존재한다.
직렬 가능한 스케줄이 항상 보장되지 않는다 → 2단계 잠금 규약(2PL)으로 해결
교착상태(deadlock)가 발생할 수 있다
단순 잠금 연산만으로는 항상 직렬 가능한 스케줄을 보장하지 못하기 때문에 2단계 잠금 규약(2PL)을 사용하여 이를 해결한다.
※ 교착상태 문제
트랜잭션 T14와 T15가 동시에 실행하는데 T14에서 write(x) 연산과 X-lock(y) 연산 사이에 T15가 실행된다고 가정해보자.
(T14) X-lock(x) ··· T14가 x에 대해 X-lock을 얻고
(T15) S-lock(y) ··· 후에 T15가 y에 대해 S-lock을 얻는다.
여기까지는 서로 다른 데이터 항목 x, y에 대해 잠금을 설정한 것이므로 문제가 없다.
(T15) S-lock(x) ··· 그 다음 T15가 x에 대해 S-lock를 요청했을 때, T14가 이미 X-lock을 보유하고 있기 때문에 T14가 unlock하기 전까지 대기해야 한다.
(T14) X-lock(y) ··· T14가 y에 대해 X-lock을 요청하면, 이미 T15가 y에 대한 S-lock을 얻었기 때문에 T14 또한 T15가 unlock하기 전까지 대기하게 된다.
결국 T14, T15가 모두 대기 상태에 들어가 더 이상 진행하지 못하게 되고, 이런 상태를 교착상태(deadlock)라고 한다.
교착상태에 빠지면 외부에서 강제로 트랜잭션을 중단하거나 잠금을 해제하지 않는 이상 무한정 대기 상태로 남게 된다.
2PL은 잠금을 설정하는 단계와 해제하는 단계로 나누어 수행한다.
확장단계(growing phase) : 트랜잭션이 lock 연산만 수행할 수 있고 unlock 연산은 수행할 수 없는 단계
축소단계(shrinking phase) : 트랜잭션이 unlock 연산만 수행할 수 있고 lock 연산은 수행할 수 없는 단계
2PL은 데이터 오류 가능성을 사전에 예방할 수 있고 알고리즘이 간단하며 직렬 가능한 스케줄을 보장하는 방법 중 하나이다.
하지만 2PL로도 교착상태 문제는 여전히 해결되지 않는다.
오른쪽 그림이 2PL을 준수하지만 교착상태가 발생한 사례에 해당한다. 이 문제를 해결하는 가장 간단한 방법으로는 각 트랜잭션을 시작하기 전에 모든 필요한 잠금을 동시에 설정하는 방법이 있다.
또는 교착상태 회피 방법이나 탐지 방법을 통해 교착상태를 해결해야 한다.
2PL은 연쇄 복귀 문제도 발생할 수 있다.
이는 엄격한 2PL(strict 2PL)로 해결 가능하다.
모든 X-lock에 대한 unlock 연산을 트랜잭션이 완전히 완료된 후에 실행하는 것이다.
이렇게 하면 완료되지 않은 트랜잭션에 의해 갱신된 데이터를 다른 트랜잭션이 읽거나 쓸 가능성을 원천적으로 봉쇄할 수 있어 연쇄 복귀 문제를 해결할 수 있다.
현재 대부분의 DBMS에서 엄격한 2PL 규약을 이용하여 동시성 제어를 구현한다.