Lock을 활용한 concurrency control

정민교·2023년 6월 23일
0

DB

목록 보기
8/12
post-thumbnail

📒

지난 포스팅에서는 여러 트랜잭션이 겹쳐서 실행될 때 발생할 수 있는 이상 현상들에 대해서 알아보았고,

그 이상 현상들을 정의한 SQL표준과 이 SQL표준을 비판한 논문에서 다룬 이상 현상들도 살펴보았습니다.

또한 이러한 이상 현상을 기준으로 어떤 이상 현상까지 허용하는 지를 결정하는 isolation level도 살펴보았습니다.

오늘은 Lock과 concurrency control에 대해서 알아보겠습니다.

✔️Lock

데이터베이스에서 어떤 데이터에 대한 읽기 혹은 쓰기 작업을 하려면 Lock을 취득해야 합니다.

Lock을 사용하는 이유는 데이터를 보호하기 위함입니다.

이와 관련되서 비슷한 예는 파일을 오픈되어 있는 상태라면 삭제가 안 되거나, 공유 파일일 경우에 다른 사람이 수정 작업을 하고 있는 경우에는 읽기 전용으로는 파일을 열 수 있다거나 하는 것과 비슷합니다.

데이터 베이스에 다음과 같은 데이터가 있습니다.

x=10

tx1
x를 20으로 바꾼다.

tx2
x를 90으로 바꾼다.

다음과 같은 schedule이 있습니다.

write_lock(tx1, x) - write_lock(tx2, x) - write(tx1, x=20) - unlock(tx1, x) - write(tx2, x=90) - unlock(tx2, x)
										  x=20							      x=90

tx1은 x에 대한 쓰기 작업을 해야하니까 x에 대해 write_lock을 취득합니다.

그리고 바로 다음에 tx2가 x에 대해 write_lock을 취득하려 시도합니다.

하지만 tx1이 x에 대한 write_lock을 점유하고 있어서 write_lock을 취득할 수 없고, tx1이 x에 대한 write_lock을 반환할 때까지 기다려야 합니다.

tx1은 x=20으로 바꾸는 쓰기 작업을 완료하고 lock을 반환합니다.

그러면 tx2는 x에 대한 쓰기 락을 취득하고 x=90으로 바꾸는 쓰기 작업을 완료하고 lock을 반환합니다.

이번엔 다른 트랜잭션들을 예로 살펴봅시다.

tx1
x를 20으로 바꾼다.

tx2
x를 읽는다.

write_lock(tx1, x) - read_lock(tx2, x) - write(tx1, x=20) - unlock(tx1, x) - read(tx2, x) - unlock(tx2, x)

tx1이 x에 대한 쓰기 작업을 위해 쓰기 락을 취득합니다.

그 후에 tx2가 x에 대한 읽기 락을 취득하려하는데 tx1이 x에 대한 쓰기 락을 점유하고 있어서 읽기 락을 취득할 수 없습니다.

tx1은 계속해서 트랜잭션을 진행할 수 있고 x=20쓰기 작업 후 lock을 반환합니다.

tx2는 읽기 락을 취득하고 x에 대한 읽기 작업을 완료한 다음 lock을 반환합니다.

📌write-lock(exclusive lock)

쓰기 락은 read 뿐만 아니라, insert, update, delete등의 쓰기 작업을 할 때도 사용합니다.

또한 어떤 트랜잭션이 어떤 데이터에 대한 쓰기 락을 취득했다면 다른 트랜잭션이 해당 데이터에 대한 읽기, 쓰기 작업을 허용하지 않습니다.

중요한 건 exclusive 목적의 lock이기 때문에 나 빼고는 아무도 못 쓴다는 것입니다. 또한 쓰기 락은 획득했으면 읽기, 쓰기 모두 가능합니다.

📌read-lock(shared lock)

읽기 락은 읽기 목적의 락입니다. 데이터를 read할 때 사용하며, 이미 한 트랜잭션이 x 데이터에 대한 읽기 락을 점유하고 있어도 다른 트랜잭션이 같은 데이터에 대해 읽기 작업 하는 것을 허용합니다.

읽기는 데이터를 변경하는 것이 아니기 때문에 얼마든지 읽기를 허용하는 것은 상관 없습니다.

하지만 한 트랜잭션이 데이터에 대해 read lock을 취득한 상태인데 다른 트랜잭션이 같은 데이터에 대해 쓰기 작업을 하려고 한다면 read lock이 하지 못하게 막습니다. 현재 트랜잭션이 읽은 데이터를 보호해야 하기 때문입니다.

📌read-lock과 write-lock의 관계

read-lockwrite-lock
read-lockOX
write-lockXX

어떤 트랜잭션이 read-lock을 쥐고 있는데 다른 트랜잭션이 같은 데이터에 대해 read-lock을 쥐려고 한다면 이는 허용합니다.

하지만 어떤 트랜잭션이 write-lock을 쥐고 있는데 다른 트랜잭션이 같은 데이터에 대해 read-lock을 쥐려고 하는 경우, 그리고 그 반대의 경우는 허용하지 않습니다. 따라서 이 같은 경우는 lock을 쥐고 있는 트랜잭션이 lock을 반환할 때까지 기다려야합니다.

lock을 통해 어떻게 concurrency control을 구현해서 serializability를 보장할 수 있는지(어떻게 isolation을 보장할 수 있는지) 알아봅시다.

📌lock을 사용해도 발생하는 이상 현상과 해결 방법

👉이상현상

다음과 같은 예를 살펴봅시다

데이터베이스

x=100, y=200

tx1. x와 y의 합을 x에 저장

A. read_lock(y)
B. read(y)
C. unlock(y)
D. write_lock(x)
F. read(x)
G. write(x=x+y)
H. unlock(x)

tx2. x와 y의 합을 y에 저장

A. read_lock(x)
B. read(x)
C. unlock(x)
D. write_lock(y)
F. read(y)
G. write(y=x+y)
H. unlock(y)

이 두 트랜잭션이 serial schedule로 동작할 경우의 결과를 살펴봅시다.

tx1 - tx2 => x=300, y=500
tx2 - tx1 => x=400, y=300

다음과 같은 schedule이 있습니다.

tx2(A) - tx2(B) - tx2(C) - tx1(A) - tx2(D) - tx2(F) - tx2(G,y=300) - tx2(H) - tx1(B,y=200) - tx1(C) - tx1(D) - tx1(F) - tx1(G,x=300) - tx1(H)

결과적으로 x=300, y=300이 되었습니다.

serial schedule인 두 schedule과는 다른 결과가 나왔습니다. 즉 두 트랜잭션이 겹쳐서 실행된 위 schedule은 nonserializable합니다.

따라서, lock을 사용하는 것 만으로는 serializable을 만족시킬 수가 없습니다.

자 그럼 왜 위 schedule이 serializable하지 않았을까요.

tx1은 x를 업데이트, tx2는 y를 업데이트 하는 트랜잭션입니다.

tx2가 먼저 시작을 하면서 업데이트 되기 전의 x를 읽었습니다. 그럼 tx2는 업데이트 된 후의 y를 읽어야 serializable하게 동작할 것입니다.

하지만 tx1이 y에 대한 read_lock을 먼저 점유하는 바람에 y에 대한 업데이트를 해야하는 tx1이 y에 대한 write-lock을 점유하지 못하고 기다리게 됩니다.

그리고 tx1은 y에 대한 read_lock을 취득했으니 이 당시의 y 값인 200을 읽게 되는 것입니다.

그래서 결국 문제는 tx2(C) - tx1(A) - tx2(D) 이 세 부분에 존재하게 됩니다.

그럼 이 문제를 어떻게 해결할 수 있을까요.

👉해결 방법

tx2가 x에 대한 lock을 반환하기 전에 y에 대한 write-lock을 먼저 취득한 후에 x에 대한 lock을 반환하도록 두 부분( tx2(C)와 tx2(D) )의 순서를 바꿔주면 됩니다.

그러면 tx1은 y에 대한 read-lock을 취득하고 싶어도 tx2가 이미 y에 대한 write-lock을 점유하고 있기 때문에 lock을 취득할 수 없습니다.

그럼 위 schedule의 operation 실행 순서가 변하게 됩니다.

tx2(A) - tx2(B) - tx2(D) - tx1(A) - tx2(C) - tx2(E) - tx2(F) - tx2(G) - tx1(B) - tx1(D) - tx1(C) ....

이렇게 되면 serial schedule과 동일하게 됩니다.

위 schedule을 자세히 보면 tx1도 write-lock을 획득하고 read-lock을 반납하는 operation 실행 순서를 변경하여 적었습니다.

이렇게 변경해야 tx1이 먼저 시작하더라도 아까 같은 이상현상이 발생하지 않기 때문입니다.

결국 두 트랜잭션 operation의 순서를 다음과 같이 바꿔주게 된 것입니다.

tx1. x와 y의 합을 x에 저장

A. read_lock(y)
B. read(y)
D. write_lock(x)
C. unlock(y)
F. read(x)
G. write(x=x+y)
H. unlock(x)

tx2. x와 y의 합을 y에 저장

A. read_lock(x)
B. read(x)
D. write_lock(y)
C. unlock(x)
F. read(y)
G. write(y=x+y)
H. unlock(y)

👉정리

자 원래 tx1과 tx2의 operation 실행 순서로 실행하면 두 트랜잭션이 겹쳐서 실행되는 schedule에서 이상현상이 발생할 수 있다는 것을 알았습니다.

그 해결 방법으로 쥐고 있던 lock을 먼저 반납하는 것이 아닌 다른 lock을 획득한 후에 쥐고 있던 lock을 반납하도록 바꾸었더니 이상현상을 해결할 수 있었습니다.

📌two-phase locking protocol

2PL protocol에 대해서 알아봅시다.

2PL protocol은 트랜잭션에서 모든 locking operation이 최초의 unlock operation 보다 먼저 수행되도록 하는 프로토콜입니다.

위에서 이상 현상을 해결하기 위해 locking operation의 실행 순서를 변경한 것이 2PL protocol입니다.

그럼 저 두 트랜잭션의 operation 실행 순서를 잘 보면 lock을 획득하는 부분과 반환하는 부분으로 나누어져 있는 것을 확인할 수 있습니다.

👉expanding phase(growing phase)

operation 실행 순서를 봤을 때 read-lock을 쥐고 write-lock을 쥐는 부분까지를 expanding phase라고 부릅니다.

lock을 취득하기만 하고 반환하지는 않는 phase를 의미합니다.

👉shirinking phase(contracting phase)

그 다음 read-lock과 write-lock을 반환하는 부분을 shrinking phase라고 합니다.

lock을 반환만하고 취득하지는 않는 phase를 의미합니다.

👉정리

2PL protocol은 트랜잭션에서 모든 locking operation이 최초의 unlock operation 보다 먼저 수행되도록 하는 프로토콜입니다.

expanding phase와 shirinking phase로 나뉘며, 정리해보면 locking operation만 발생하다가(expanding phase) 최초의 unlock operation이 발생하는 시점부터는 더 이상 locking operation이 발생하지 않고 unlock operation만 발생합니다(shirinking phase).

또한 2PL protocol은 각각의 트랜잭션에 대해 serializability를 보장합니다.

📌2PL & deadlock

2PL 프로토콜로 동작하더라도 어떤 경우에 따라서는 이상현상이 발생할 수 있습니다.

tx1. x와 y의 합을 x에 저장

A. read_lock(y)
B. read(y)
C. write_lock(x)
D. unlock(y)
F. read(x)
G. write(x=x+y)
H. unlock(x)

tx2. x와 y의 합을 y에 저장

A. read_lock(x)
B. read(x)
C. write_lock(y)
D. unlock(x)
F. read(y)
G. write(y=x+y)
H. unlock(y)

2PL 프로토콜로 두 트랜잭션이 겹쳐서 실행될 때 이상현상이 발생할 수 있습니다.

tx1(A) - tx1(B) - tx2(A) - tx2(B) - tx1(C) - tx2(C)...

자 여기서 tx1이 read-lock을 쥐고 y의 데이터를 읽었습니다. 그 후에 바로 tx2가 read-lock을 쥐고 x의 데이터를 읽었습니다.

그리고 tx1이 x에 대한 write-lock을 쥐려고 하는데 이미 tx2가 x에 대한 read-lock을 쥐고 있어서 write-lock을 취득하지 못하고 기다립니다.

그 다음에 tx2가 y에 대한 write_lock을 쥐려고 하는데 이미 tx1이 y에 대한 read-lock을 쥐고 있어서 write-lock을 취득하지 못하고 기다립니다.

결국 tx1과 tx2 모두 lock이 해제되기를 기다리면서 멈춰버리는 deadlock 현상이 발생했습니다.

이 deadlock 현상은 운영체제에서 deadlock을 해결하는 방법과 비슷합니다.

✔️정리

트랜잭션 isolation을 보장하기 위해 concurrency control이 필요합니다 그리고 concurrency control은 lock을 통해서 구현할 수 있습니다.

2PL protocol을 따르도록 RDBMS를 설계하면 이 RDBMS는 concurrency control을 통해 serializability를 보장합니다.

다음 포스팅에서는 2PL protocol의 종류에 대해 알아보겠습니다.

profile
백엔드 개발자

0개의 댓글