티스토리 뷰
운영체제에서 여러 스레드가 동일한 자원에 접근할 때 동시성 문제가 발생할 수 있는 것처럼 데이터베이스에서도 여러 트랜잭션이 동시에 실행되고 이 트랜잭션들이 동일한 자원에 접근할 때 동시성 문제가 발생한다.
이를위해 데이터베이스에서도 락을 제공해 여러 트랜잭션이 동시에 데이터를 처리할 때, 데이터의 무결성과 일관성을 보장할 수 있도록 돕는다.
Lock의 수준
- Database Level
- 전체 데이터베이스에 대한 락을 설정하는 가장 큰 범위의 락이다.
- 관리 비용이 매우 적고 데이터베이스 백업 또는 복구와 같은 특정 유지보수 작업 시에 유용하다.
- 하지만 동시성이 전혀 보장되지 않아 작업 중인 트랜잭션 외의 모든 트랜잭션은 대기해야한다.
- Table Level
- 테이블 전체에 대해 락을 설정한다.
- 매우 간단하고 관리 비용도 적다. 테이블 전체가 잠기므로 락 관리가 쉽다.
- 한 트랜잭션이 테이블에 락을 걸기 때문에 다른 트랜잭션은 해당 테이블의 데이터를 읽거나 수정할 수 없어 동시성이 크게 감소한다.
- 대량의 데이터를 일괄 처리하거나, 시스템 유지보수 시 주로 사용된다.
- Page Level
데이터베이스 page는 데이터를 저장하고 관리하는 기본 단위이다. 디스크에서 데이터를 읽고 쓸 때 효율성을 높이기 위해 설계된 고정된 크기의 블록이다. 이 페이지 내에 여러 개의 row가 포함되며 row들은 연속적으로 저장될 수 있다.
- 데이터베이스 페이지에 락을 설정하는 중간 수준의 락이다.
- 행 락보다 관리 비용이 적지만 그래도 동시성은 비교적 높다.
- Row Level
- 특정 행(레코드)에만 락을 설정하는 가장 세밀한 수준의 락이다.
- 같은 테이블의 같은 페이지더라도 다른 행이라면 다른 트랜잭션의 접근을 막지 않아 동시성을 극대화할 수 있다.
- 하지만 많은 수의 행에 락이 설정되면 락을 관리하기 위해 많은 시스템 비용과 메모리가 소모된다.
- 쿼리의 유형과 데이터베이스 설정에 따라 달라질 수 있지만 일반적으로 가장 많이 사용되는 락 수준이다.
Lock의 종류
1. Shared Lock(S-Lock, 공유 락)
공유 락은 읽기 전용 락이다. 여러 트랜잭션이 `select` 쿼리로 동시에 데이터를 읽는 것은 가능하지만 데이터 수정은 불가능하기 때문에 여러 트랜잭션이 동시에 데이터를 읽어도 일관성에는 아무런 영향을 미치지 않는다. `Read Lock`이라고도 불린다.
2. Exclusive Lock(X-Lock, 베타 락)
베타 락은 데이터를 수정하기 위한 락이다. `insert`, `update`, `delete`와 같이 데이터를 수정하는 트랜잭션에서 사용되며 베타 락이 걸린 범위에 대해서는 다른 트랜잭션은 읽거나 수정할 수 없다. `Write Lock`이라고도 불린다.
3. Update Lock(U-Lock, 업데이트 락)
업데이트 락은 공유 락과 베타 락의 중간 단계에 해당하는 락으로 데이터를 업데이트할 가능성이 있는 경우에 걸리는 락이다. 업데이트 락이 걸린 자원에 대해서는 락을 건 트랜잭션 외에는 자원에 베타 락을 걸 수 없다.
다음과 같은 쿼리가 수행된다고 생각해보자.
UPDATE usr SET age = 10 WHERE name = "A";
업데이트 락이 사용되어 위의 쿼리가 실행되는 일반적인 시나리오는 다음과 같다.
- 트랜잭션 시작
- 업데이트 락 설정 - 쿼리가 실행되기 전에 `WHERE`의 조건에 맞는 행을 찾는 과정에서 다른 트랜잭션이 이 데이터를 수정하지 못하도록 업데이트 락을 설정한다.
- 공유 락 설정 (필요한 경우) - `WHERE` 조건에 만족하는 행을 찾기 위해 먼저 해당 행을 읽어야 한다. 이를 위해 먼저 해당 행에 대해 공유 락을 설정한다.
- 베타 락 설정 - 데이터를 수정하기 위해서는 이 행에 베타 락을 설정해야 한다. 베타 락이 설정되기 위해서는 먼저 공유 락이 해제되어야 한다.
- 데이터 업데이트
- 락 해제
위의 시나리오만 보면 사실 업데이트 락의 이점을 잘 파악하지 못할 수도 있다. 업데이트 락이 없다고 가정하고 다음과 같은 상황을 생각해보자.
T-A: UPDATE usr SET age = 10 WHERE name = "A";
T-B: UPDATE usr SET age = 20 WHERE name = "A";
`T-A`와 `T-B`는 동시에 실행된다. 두 트랜잭션이 시작하면 먼저 `WHERE`의 조건에 만족하는 행을 읽기 위해 조건에 맞는 행에 S-Lock을 설정한다. 이 후 두 트랜잭션은 데이터를 업데이트하기 위해 X-Lock을 시도한다. X-Lock이 설정되기 위해서는 먼저 해당 행에 S-Lock이 모두 해제되어야 한다.
여기서 중요한 점은 X-Lock을 시도하기 위해 S-Lock이 먼저 해제되지 않는다는 것이다. 때문에 다음과 같은 상황이 발생한다.
- `T-A`는 X-Lock을 요청하지만, 현재 `T-B`가 X-Lock을 설정하려고 대기하고 있기 때문에 `T-B`는 `T-A`가 S-Lock을 해제하기를 기다린다.
- `T-A`는 X-Lock을 요청하지만, 현재 T-A가 X-Lock을 설정하려고 대기하고 있기 때문에 `T-A`는 `T-B`가 S-Lock을 해제하기를 기다린다.
즉, `T-A`와 `T-B가` 서로의 S-Lock을 해제하기를 기다리는 데드락 상태가 발생하게 된다. 이러한 데드락을 Conversion Deadlock이라 한다.
이제 여기서 업데이트 락을 적용해보자. 아주 조금 빨리 `T-A`가 먼저 쿼리문에 도달했다고 가정하자. 그럼 다음과 같은 과정으로 실행될 것이다.
- `T-A`, `T-B`: 시작
- `T-A`: 자원에 대해 U-Lock 설정
- `T-B`: 자원에 U-Lock이 설정되었기 때문에 대기
- `T-A`: 조건에 맞는 행에 S-Lock을 걸어 데이터를 읽음
- `T-A`: S-Lock 해제 후 X-Lock으로 전환해 데이터 수정
- `T-A`: commit
- `T-B`: 조건에 맞는 행에 S-Lock을 걸어 데이터를 읽음
- `T-B`: S-Lock 해제 후 X-Lock으로 전환해 데이터 수정
- `T-B`: commit
3번 과정에서 자원은 `T-A`에 의해 U-Lock이 걸려있기 때문에 데이터를 수정할 의도를 가지고 있는 `T-B`의 U-Lock은 대기 된다. 때문에 먼저 `T-A`가 트랜잭션을 수행하게 되고 `T-B`는 `T-A`가 커밋되어 걸려있던 락이 해제된 후 실행된다. 이 덕분에 데드락을 방지할 수 있게 된다.
4. Intent Lock(내재 락/의도 락)
Intent Lock은 데이터베이스에서 트랜잭션의 락 설정을 효율적으로 관리하기 위해 사용하는 락이다. 예를 보는 것이 이해가 빠를 것이다.
트랜잭션 A에서 다음과 같은 쿼리를 실행한다면 users 테이블의 id = 1에 해당하는 레코드에 X-Lock이 걸릴 것이다.
UPDATE users SET age = 30 WHERE id = 1;
이후 트랜잭션 B에서 테이블 변경 등의 이유로 테이블 수준의 락을 설정하려고 한다. 이를위해 트랜잭션 B는 테이블 내에 락이 걸려있는지 모두 확인해야 하는 과정을 거쳐야하므로 비효율적이 작업이 될 수 있다.
이렇게 Intent Lock은 하위 수준에서 락을 걸겠다는 의도를 상위 수준에 미리 명시하여 락의 경합을 줄여 효율성을 높이는 것이다. 예를들어 특정 레코드에 락을 걸때 상위 수준인 테이블에 대해 Intent Lock을 먼저 설정함으로써 다른 트랜잭션이 테이블에 락을 걸지 못하도록 한다.
Intent Lock은 S, X-Lock 앞에 I를 붙여 표현한다.
- IS-Lock(Intent Shared Lock)
- 트랜잭션이 하위 자원에 공유 락을 설정할 의도가 있다.
- ex) 트랜잭션이 테이블의 특정 레코드에 공유 락을 설정할 때 먼저 테이블에 IS-Lock을 설정
- IX-Lock(Intent Exclusive Lock)
- 트랜잭션이 하위 자원에 베타 락을 설정할 의도가 있다.
- ex) 트랜잭션이 테이블의 특정 레코드에 베타 락을 설정할 때 먼저 테이블에 IX-Lock을 설정
- SIX-Lock(Shared Intent Exclusive Lock)
- 트랜잭션이 상위 자원에 대해 공유 락을 설정하고, 하위 자원에 대해 베타 락을 설정할 의도가 있다. 이는 동일한 트랜잭션 내에서 상위 자원을 공유하고, 특정 하위 자원은 베타적으로 수정할 수 있게 한다.
- ex) 트랜잭션이 테이블 전체에 대해 공유 락을 설정하고, 특정 해에 베타 락을 설정하려는 경우 SIX-Lock 설정
Lock Escalation
Lock 또한 메모리 자원을 사용하게 되며 데이터베이스가 락을 관리하기 위해 시스템 자원이 소모된다.
예를들어 다음과 같은 쿼리의 실행으로 약 5000개의 행에 락이 걸린다고 생각해보자.
UPDATE orders SET status = 'shipped' WHERE order_id BETWEEN 1000 AND 6000;
이를 위해 5000개의 락만큼의 메모리와 관리하기 위한 시스템 자원이 필요하게 되고 그만큼 관리에 대한 복잡성이 높아지게 되어 메모리 부족과 성능 저하 등의 문제가 발생할 수 있다. 이를 해결하기 위해 데이터베이스는 특정 기준을 만족하면, 여러 개의 하위 수준의 락을 상위 수준의 하나의 락으로 에스컬레이션하여 관리 효율성을 높인다.
ex) record → page or table
락 에스컬레이션은 일반적으로 다음과 같은 상황에서 발생한다.
- 하나의 트랜잭션이 특정 테이블 혹은 페이지의 많은 행에 대해 락을 설정하려는 경우 상위 수준의 락으로 에스컬레이션을 수행한다.
- 데이터베이스 시스템은 일반적으로 임계값을 정해두고 트랜잭션이 설정한 락의 개수가 이 임계값을 초과하면 락 에스컬레이션을 수행한다.
락 에스컬레이션을 통해 수천 개의 락을 개별적으로 관리하는 대신 상위 수준의 하나의 락으로 처리함으로써 메모리와 CPU 사용을 줄여 성능의 향상과 관리 효율성을 높인다. 하지만 상위 수준의 락으로 에스컬레이션하는 만큼 다른 트랜잭션이 해당 상위 수준에 속하는 데이터에 접근하는 것이 제한되어 동시성이 감소할 수도 있다.
'CS > Database' 카테고리의 다른 글
Database Concurrency Control (0) | 2024.11.05 |
---|---|
Transaction (2) | 2024.10.25 |