🔒 Lock
- write 연산은 단순히 값 하나 바꾸는 것보다 더 복잡한 과정
- 인덱스 처리, 실제 파일에 대한 처리 등이 포함될 수 있음
- 동시에 같은 데이터에 또 다른 read / write가 있다면 예상치 못한 동작을 할 수 있다.
=> LOCK을 통해 해결할 수 있다!
Write-Lock(Exclusive Lock, 배타적 락)
- read / write(insert, modify, delete)할 때 사용한다
- write-lock이라고 해서 무조건 write 할 때만 사용하는 것이 아니라는 것에 주의!
- 다른 tx가 같은 데이터를 read / write 하는 것을 허용하지 않는다.
Read-Lock(Shared Lock, 공유 락)
- read 할 때 사용한다
- 다른 tx가 같은 데이터를 read하는 것은 허용한다.
Lock 호환성
- Read-Lock끼리만 동시 호환이 가능하며, 나머지는 한 쪽이 끝날 때까지 대기해야 한다.
- Write-Lock은 다른 Lock과 호환되지 않아 동시 사용이 불가능하다.
- 어느 트랜잭션이 데이터를 읽고 있다면 다른 트랜잭션은 해당 데이터에 대한 쓰기 작업을 할 수 없다.
참고로 획득한 락을 해제하는 방법은 결국 커밋과 롤백 밖에 없다
하지만, LOCK을 사용하는 것만으로는 트랜잭션의 serializability를 보장할 수 없다.. -> 여전히 이상 현상이 발생할 수 있다.
간단하게만 설명하면, x와 y라는 데이터가 있고, t1은 x와 y의 합을 x에 저장하는, t2는 x와 y의 합을 y에 저장하는 트랜잭션이라고 하자.
- t1이 먼저 y의 값을 읽기 위해 read_lock(y)을 얻으려고 시도한다. y를 사용하는 트랜잭션이 아직 없으므로 락을 얻는다.
- t1은 y 값 읽기에 성공하면 unlock(y)를 통해 읽기 락을 반환한다.
- t2가 그 사이 x에 대한 값을 읽기 위해 read_lock(x)을 얻으려고 시도한다. x를 사용하는 트랜잭션이 아직 없으므로 락을 얻는다.
- t1이 x의 값을 업데이트 하기 위해 write_lock(x)를 얻으려고 시도한다. 이때, t2가 이미 x에 대한 읽기 락을 소유하고 있으므로, t1은 x에 대한 쓰기 락을 획득할 수 없다.(락 호환성 문졔) -> 락을 얻을 때까지 대기한다.
- t2는 x의 값을 읽고 락을 반납한다.
- t1은 x에 대한 write_lock(x)를 얻는다. x의 값을 읽고 값을 업데이트 후, 락을 반환한다.
- t2는 y에 대한 write_lock(y)를 얻으려고 시도한다. y를 사용하는 트랜잭션이 없으므로, 락을 얻는다.
- t2는 y의 값을 읽고 업데이트 후, 락을 반환한다.
위와 같은 과정이 있다고 했을 때, 결과는 x = 300, y = 500이 아닌 둘다 300이 되는 이상 현상이 발생한다.
이러한 문제가 발생한 이유는, 흰색 빗금친 부분으로 인한 것이다.
t1이 y에 대한 unlock을 먼저 한 후, 이후 x에 대한 write lock을 획득하려고 시도하는데, 이 과정에서 t2가 먼저 x에 대한 read_lock을 획득하는 것이 문제이다. 만약 t1의 unlock(y)와 write_lock(x)의 순서를 변경한다면 이러한 문제는 발생하지 않을 것이다.
t2 역시 마찬가지다. unlock(x)와 write_lock(y)의 순서를 바꿔준다면 실행 순서가 반대인 경우에도 이상 현상이 발생하지 않을 것이다.
=> 트랜잭션에서 모든 locking operation(락을 획득하는 작업)이 최초의 unlock operation보다 먼저 수행되도록 해야 한다
=> 2PL protocol(Two-phase locking)
2PL Protocol
2PL protocol(two-phase locking)
: tx에서 모든 locking operation이 최초의 unlock operation 보다 먼저 수행되도록 하는 것Expanding phase(growing phase)
: lock을 취득하기만 하고, 반환하지는 않는 phaseShrinking phase(cnostracting phase)
: lock을 반환만 하고, 취득하지는 않는 phase- Expanding phase가 먼저 나오고 이후에 Shrinking phase가 나오게 함
- 한번 unlock을 시작하면 그 이후에는 새로운 lock을 취득하지 않는다
- 2PL protocol은 serializability를 보장해준다!
2PL Deadlock
하지만 이러한 경우에도 특별한 예외 상황은 생길 수 있다. -> 2PL Deadlock
- 2PL 방식을 따르다보면 상황에따라 서로가 서로에게 block되어서 진행되지 못하는
deadlock
상태가 발생할 수도 있다. - OS에서 발생하는 deadlock과 비슷한 상태로 해결 방법 역시 비슷하다.
중간 정리
트랜잭션의 Isolation을 보장하기 위해서 Concurrency Control이 필요하다.
그리고 이 Concurrency Control을 Lock을 통해 구현한 방법이 2PL protocol을 따르는 Lock 방식이다.
이는 serializability를 보장한다.
다양한 2PL 종류
Conservative 2PL
- 모든 lock을 취득한 뒤에 트랜잭션을 시작한다.
- 즉, 모든 lock을 취득하는 부분을 스케줄의 제일 앞단으로 모두 당긴다
- deadlock이 발생하지 않는다
- 트랜잭션을 시작하기 위해 모든 lock을 획득하고 난 뒤에야 시작할 수 있기 때문에 실용적이진 않다..
Strict 2PL(S2PL)
- strict schedule을 보장하는 2PL
- Strict Schedule: 어떤 데이터에 대해 write하는 트랜잭션이 있다면 그 트랜잭션이 커밋 혹은 롤백되기 전까지 다른 트랜잭션이 그 데이터에 대해 읽거나 쓰기를 하지 않는 스케줄
- recoverability를 보장
write-lock
을 commit / rollback 될 때 반환(unlock)
- strict schedule을 보장하는 2PL
Strong Strict 2PL (SS2PL or rigorous 2PL)
- strict schedule을 보장하는 2PL
- recoverability를 보장
read-lock / write-lock
모두 commit / rollback 될 때 반환(unlock)- S2PL보다 구현이 쉽다.
- 하지만 lock을 오래 쥐고 있게 되므로, 다른 트랜잭션들의 대기 시간이 증가하게 될 것이다.
=> Recoverability는 매우 중요하므로 보통 S2PL이나 SS2PL이 초창기 RDBMS에서 가장 많이 쓰임
Lock 호환성 방식의 약점
read-read를 제외하고는 한 쪽이 block이 되니까 전체 처리량이 좋지 않다..
→ write-write는 어쩔 수 없더라도 read와 write가 서로를 block 하는 것이라도 해결해보자..
→ 그것이 바로 MVCC
→ 오늘날의 많은 RDBMS가 LOCK과 MVCC를 혼용해서 사용한다.
정리
- concurrency control을 구현하기 위해
LOCK
을 사용했다. - 그리고 concurrency control의
serializability
를 보장하기 위해2PL protocol
이 사용됐다. - serializability 뿐만 아니라
recoverability
까지 보장하기 위해 2PL 중에서 S2PL이나 SS2PL이 많이 사용됐다. - 하지만, 이후에는 성능 이슈로 인해 성능을 올리기 위해
MVCC
를 적용했다.
'Database' 카테고리의 다른 글
[Database] Index (0) | 2024.10.07 |
---|---|
[Database] MVCC (2) | 2024.10.07 |
[Database] Transaction Isolation Level (0) | 2024.10.06 |
[Database] Transaction, Concurrency Control (1) | 2024.10.06 |
[Database] 테이블 설계 / FD / DB 정규화(Normalization) (4) | 2024.10.05 |