이전 글에서 이어지는 내용이다.
2025.11.21 - [데이터베이스 시스템] - [DB System 이론] Concurrency Control
[DB System 이론] Concurrency Control
이전 글을 보고 오면 아래 내용을 더 편하게 읽을 수 있다.2025.11.18 - [데이터베이스 시스템] - [DB System 이론] Transaction & Conflict Serializability [DB System 이론] Transaction & Conflict Serializabilitytransaction은 기
april2901.tistory.com
concurrent한 schedule이 좋다고는 했지만 serial schedule와 비교했을 때 명령어의 개수가 줄어들지 않는다.
실제로 schedule talbe을 봤을 때 테이블의 높이가 줄어들지 않았다.
이게 좋다고 하는 이유는 병렬로 동시에 실행되기 때문이다.
테이블에서 conflitct가 발생하지 않는 선에서 위로 밀착시키는 것과 같다.
Lock-based Protocols
lock은 '내가 이 변수를 건들 것이니 다른 애들은 건들지 마'라는 독점권이라고 생각하면 된다.
OS를 공부했다면 lock에 대해 어느정도 알 것이다.
하지만 OS와 달리 DB에는 read가 매우 많아서 OS처럼 lock을 잡으면 성능이 많이 떨어진다.
그래서 lock을 두 종류로 나눴다.
1. eXclusive lock: write가 있을 때 잡는 lock이다. X lock이라고도 표현한다.
2. Shared lock : 읽기만 할 때 잡을 lock이다. S lock이라고도 표현한다.
lock은 트랜잭션이 '나는 이 변수에 쓰기를 할거니까 X lock을 잡아야지'라고 생각한다고 바로 잡을 수 있는 것이 아니다.
트랜잭션은 concurrency-control manager에게 lock을 달라고 한다.
따라서 lock-X는 이 친구한테 lock을 요청하는 명령어이고, 그 허락여부가 이 명령의 반환값으로 오게 된다.
아래 표는 어떤 lock이 잡혀있을 때 다른 lock을 요청할 수 있는지 없는지를 보여준다.
어떤 변수를 동시에 읽는 것은 가능하기 때문에 한 변수에 대해 S lock은 여러개 중복될 수 있다.
나머지의 경우에는 불가능하다.

아래 예시는 lock 명령어와 concurrency-control manager의 동작을 표시한 스케줄이다.
이 글의 후반부에 헷갈릴 가능성이 있어 명확히 설명하자면,
트랜잭션에서 어떤 변수를 처음 사용하기 직전~사용이 끝날때 까지 lock을 잡는데, 가장 강한 락을 잡는다.
아래 예시에서 T1이 바로 X lock을 잡은 이유가 당장은 read이지만 후에 write를 사용할 것이기 때문이다.
이 예시는 비유를 하자면 이전글에서 봤던 계좌이체 예시와 비슷하다.
T1이 계좌이체 중인데 T2가 중간에 계좌 정보를 읽은 것과 같다.
lock을 쓰는 것만으론 이런 문제가 해결이 안된다.

locking protocol
locking protocol이라는 것이 있다.
lock을 잡고 풀어주는 원칙을 규정해놓은 것인데, 이에 따라 트랜잭션이 동작하면 serializable하다.
이 프로토콜에 따라 만들어진 스케줄을 legal schedule이라고 한다.

OS에서와 마찬가지로 lock을 사용하면
deadlock과 starvation 같은 문제가 생길 수 있다.
<deadlock>
T3가 아직 B에 대해 lock을 잡고 있고
T4가 A에 대한 lock을 잡고 있다고 가정하자.
이때 각 트랜잭션이 서로 다른 lock까지 요구하면
서로 상대방이 lock을 풀기를 무한히 기다리게 된다.
<starvation>
어떤 아이템에 S lock이 있을 때, 다른 트랜잭션이 X lock을 받기 위해 S가 끝나기를 기다린다고 하자.
이떄 다른 트랜잭션들이 계속 S lock을 요청한다면, S lock이 있어도 또 다른 S lock은 중첩이 가능하므로 S lock은 계속 쌓이고,
X lock은 끝나기를 기다려야하는 대상만 계속 많아진다.
이렇게 되면 X lock은 언제 받을 수 있을 지 모른다.
Two-Phase Locking(2PL) Protocol
가장 유명하고 많이 쓰는 protocol이다.
이는 항상 conflict-serializable schedule을 만들어낸다.
이름처럼 단계가 두 개 있는데,
1단계 (Growing Phase): lock들을 계속 잡는 시기
2단계 (Shrinking Phase): lock들을 release하는 시기
즉 lock을 잡고 해제하는 시기를 명확히 나눈 것이다.
1단계에서 lock이 더 필요 없어도 반납하지 않는다.
이 간단한 규칙이 serializable하게 만드는 것을 보장한다.
lock point는 Growing Phase가 끝났을 때를 얘기한다.
lock point를 기준으로 트랜잭션들을 serial하게 실행시킬 때의 순서가 정해진다.
문제는 recoverability는 보장 못한다.
따라서 2PL을 강화시킨 프로토콜들이 나왔다.
1. Strict two-phase locking
2. Rigorous two-phase locking
1번은 X lock을 commit 후 release해야한다는 추가조건이 붙는다.
즉 commit 후에야 다른 애들이 read할 수 있는 것이다.
2번은 더 강력한 프로토콜이다.
X lock 뿐 만 아니라 S lock도 commit 후 release 해야한다.
이 두가지 강화된 프로토콜은 cascadeless schedule도 보장한다.
lock conversion

T1과 T2라는 트랜잭션을 생각해보자.
여기서 T1은 read(a1)직전에 X lock을 잡는다.
이러면 T2는 그동안 아예 a1에 대한 접근이 불가하다.
이는 성능상 너무 손해이다.
그래서 lock conversion을 사용한다.
초반에는 S lock을 잡고,
write하기 전에 X lock으로 업그레이드 한다.
이러면 더 concurrent해진다.
이 예시처럼 upgrade도 있지만 당연히 downgrade도 있다.
실제로는 rigorous two-phase locking에 lock conversion을 적용시킨 방법을 많이 사용한다.
따라서 각 phase에 할 수 있는 lock관련 동작을 정리하면 아래와 같다.

이 방식은 serializability를 보장한다.
트랜잭션들은 실행될 때 이런 lock을 얻고 푸는 과정을 알아서 할만큼 똑똑하지 않아서
그냥 read, write함수를 바꿔서 자연스럽게 lock을 확인하고 사용하는 과정을 넣어 구현한다.
아래는 read함수의 예시이다.

lock table
위에서 얘기한 concurrency control manager 안에 lock manager가 있다.
이 친구가 lock을 주고 뺏는 것을 관리한다.
이 관리를 할 때 사용하는 구조가 lock table이다.

어떤 데이터에 대해 현재 락이 걸려있는지 확인할 때 적은 비용으로 확인 가능하게 해준다.
해시 함수는 데이터의 고유 ID를 해쉬구조의 주소로 반환해준다.
이 주소로 가면 링크드 리스트로 정보들이 연결되어있다.
I는 각 자원의 식별자인데 이 밑에 어떤 트랜잭션에서 어떤 lock이 있는지 알려준다.
짙은 색이 이미 부여된 lock을 의미하고,
옅은 색이 아직 부여되지 않고 lock을 받기를 대기중인 것을 의미한다.
I23의 예시를 보면, T1,T8 두 개의 트랜잭션에 의해 이 변수(데이터)에 대한 lock이 잡혀있다.
T2는 같은 변수에 대해 자신에게도 lock을 달라고 요청 중인 상태이다.
여기서 알 수 있는 것은 T1,T8이 같이 lock을 잡았으므로 이 두 트랜잭션이 잡은 락은 모두 S lock임을 알 수 있다. X lock이 하나라도 있으면 동시에 lock을 잡을 수 없기 때문이다.