Search

Lock을 사용한 동시성 제어

락(Lock)이란?

데이터베이스는 여러 사용자들이 같은 데이터를 동시에 접근하는 상황에서, 데이터의 무결성과 일관성을 지키기 위해 락을 사용합니다.
더 자세히 말하자면
데이터베이스(Database)에서 트랜잭션(Transaction) 처리의 순차성을 보장하기 위해 데이터 변경을 일시적으로 중지하는 것
예 : 어떤 데이터 베이스의 테이블 값 x를 x=90에서 20으로 바꾸는 것
사실 값 하나 바꾸는 것보다 더 복잡한 과정일 수 있다
→ 같은 데이터에 또 다른 read, write가 있다면 예상치 못한 동작을 할 수 있기 때문!
순차적으로 write_lock을 획득함으로써 쓰기에 대한 순차성을 보장할 수 있게 된다
이때 write_lock은 어떤 트랜잭션에 대한 락을 획득하는 것
그리고 unlock을 통해 획득한 락을 반환하게 되어 해당 락을 다른 트랜잭션이 획득할 수 있게 한다
여기서의 write_lock을 베타 락(exclusive lock)이라고 부른다

베타 락(Exclusive Lock)

베타 락은 데이터에 변경을 가하는 쓰기 명령들에 대해 주어지는 락
하지만 read/write 할 때 모두 사용됨
Write Lock으로도 불리며, X로 표기.
write_lock(x)
베타 락은 이름처럼 다른 트랜잭션 세션(tx)이 해당 자원에 접근(ex, SELECTINSERT..) read, write하는 것을 막는다.
이러한 점에서 베타 락은 멀티 쓰레딩 환경에서, 임계 영역을 안전하게 관리하기 위해 활용되는 뮤텍스와 유사.
베타 락은 트랜잭션 동안 유지됨.

공유 락(Shared Lock)

공유 락은 데이터를 변경하지 않는 읽기 명령에 대해 주어지는 락
read할 때, 사용됨
read_lock(x)
아래와 같이 다른 트랜잭션이 같은 데이터를 read하는 것은 허용
하지만 write는 안된다
Read Lock이라고도 불리며 Shared의 앞 글자를 따서 주로 S로 표기
여러 사용자가 동시에 데이터를 읽어도 데이터의 일관성에는 아무런 영향을 주지 않기 때문에, 공유 락끼리는 동시에 접근이 가능.
Lock 호환성을 정리하면 아래와 같다.
Lock
read-lock
write-lock
read-lock
o
x
write-lock
x
x
그렇다면 락을 사용하는 것만으로 순차성이 보장될까
이를 위해 아래 예제를 한번 살펴보자
원래의 값은 x=100, y=200인 자원이 존재한다
위 예제는 각각 read_lock을 통해 테이블을 읽은 이후, 자원 x,y에 값을 write를 통해 수정하는 시나리오이다.
첫번째 시나리오는 가운데 락을 통해서만 스케쥴링을 시행하여 x=300, y=300이 결과값이 얻어진다
위 시나리오는 왼쪽에 명시된 2개의 serial schedule의 결과값과는 상이한 결과를 반환하게 된다.
이 결과의 의미는 위의 시나리오가 Nonserializable 이라는 것.
락만을 이용해서는 순차성을 온전히 serializable 할 수 없다는 것을 의미한다
트랜잭션의 순차성이 꼬인 이유를 자세히 보면
1.
트랜잭션 1번은 트랜잭션 2번으로 인해 업데이트된 y를 읽어야한다
2.
하지만 read lock을 write_lock(y)가 걸리기 전에 트랜잭션 1번에서 read_lock(y)가 걸리면서 트랜잭션 1번에서 unlock(y) 할 때까지 대기하는 현상이 발생
3.
위 문제를 해결하기 위해 트랜잭션 2번의 write_lock(y)와 unlock(x)을 바꿔주면 1번 트랜잭션이 2번 트랜잭션의 unlock(y)를 대기하게 되면서 t1 → t2 순서대로 진행이 될 것이다.
아래와 같이 락에 관한 오퍼레이션을 순차적으로 명시해줘야 serializable해진다
즉, 트랜잭션에서 모든 locking operation이 최초의 unlock operation 보다 먼저 수행되도록 해야 하는 것을 의미
이를 2PL protocol (Two phase locking)이라고 한다

2PL protocol (Two phase locking)

개요
2단계 잠금 프로토콜은 트랜잭션 도중에 락을 걸어서 동일한 데이터에 동시에 접근하려는 트랜잭션을 차단하여 직렬화를 보장하는 DBMS의 동시 제어 방법 → Concurrency control
트랜잭션에서 모든 locking operation이 최초의 unlock operation 보다 먼저 수행되도록 해야 하는 것
Expanding Phase (growing phase)
Lock을 취득하기만 하고 반환하지는 않는 phase
Shrinking phase (contracting phase)
Lock을 반환만 하고 취득하지는 않는 phase
2PL protocol의 문제점
2PL 프로토콜 방식은 직렬화는 보장하지만, 교착 상태가 발생하지 않도록 보장하지 못한다는 특징이 있다.
위와 같이 lock을 취득하는 phase에서 read_lock이 반환하지 못한 각각의 락으로 인해 각 트랜잭션의 write_lock이 실행되지 못하게 무한 대기에 빠지게 되는 Deadlock이 발생한다
해결 방안은 일반적으로 OS에서 다루는 deadlock 해결방안과 유사하다 (공룡책 참고) → 해결방안은 참고만 하길 권장한다
데드락 예방: 트랜잭션이 잠금을 요청할 때 데드락의 가능성을 검사하여 예방
타임아웃: 데드락 상황에서 타임아웃을 설정하여, 특정 시간 동안 잠금을 획득하지 못한 트랜잭션을 자동으로 중단시키는 방법
데드락 탐지 및 복구: 시스템이 주기적으로 데드락을 탐지하고, 데드락이 감지되면 해당 트랜잭션 중 하나 이상을 중단(롤백)하여 데드락을 해결
우선 순위 기반 방법: 트랜잭션에 우선 순위를 할당하여 높은 우선 순위를 가진 트랜잭션이 잠금을 더 쉽게 획득할 수 있도록 합니다
순서화된 자원 할당: 모든 트랜잭션이 동일한 순서로 자원(잠금)을 요청하도록 강제하여, 순환 대기 조건을 제거함

2PL protocol의 종류

Conservative 2PL
모든 Lock을 취득한 후, 트랜잭션을 시작
read-lock, write-lock이 앞부분에 배치
deadlock-free
실용적이지 않다
트랜잭션을 다 취득하기는 사실 어렵다 → 트랜잭션 자체가 시작하기 어려워짐
Strict 2PL (S2PL)
strict schedule을 보장하는 2PL
recoverability를 보장
직렬화 가능성(serializability)
동시에 실행되는 여러 트랜잭션이 데이터베이스에 일관성 있게 변경을 가할 수 있음을 보장
write-lock을 commit, rollback될 때 반환
잠금 지속: 트랜잭션이 커밋되거나 롤백될 때까지 획득한 모든 잠금을 유지
쓰기 잠금: 트랜잭션이 데이터를 수정할 때, 해당 데이터에 대한 쓰기 잠금(또는 배타적 잠금)을 획득
단점
잠금이 트랜잭션 종료 시까지 유지되기 때문에, 잠금 대기 시간과 잠금에 의한 리소스 사용이 증가
시스템의 처리량을 감소시킬 수 있으며, 특히 장기 실행 트랜잭션이 많은 경우 더욱 문제가 될 수 있다
Strong strict 2PL
strict schedule을 보장하는 2PL
recoverablity를 보장
read-lock, write-lock 모두 commit, rollback 될 때 반환하게 된다
Lock 호환성을 고민하다 보니 아래와 같은 니즈가 발생
Lock
read-lock
write-lock
read-lock
o
x
write-lock
x
x
초창기에는 위와 같이 했지만 결국, read와 write가 서로 block하는 것이라도 해결을 해야 한다는 니즈가 발생 이 것을 MVCC가 해결을 해준 것 (Multiversion concurrency control)

참고

원티드 백엔드 온보딩