0. 락(Lock)이란?
- 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 하나의 커넥션만 변경할 수 있게 해주는 기능
- 동시성을 제어하기 위한 기능
저는 처음에 DB 락을 접했을 때,
락을 이해할 때 ‘락을 획득한다’라는 의미를 ‘외부에서 잠금을 건다.’ 라는 의미로 이해해서 조금 힘들었습니다.
외부에서 잠금을 거는 것이 아니라,
‘내가 화장실 칸을 사용하기 위해 화장실 칸을 직접 들어가서 내가 잠그는 것’의 관점으로 이해하니 더 쉽게 이해할 수 있었습니다.
1. 락의 종류
락의 종류는 크게 다음과 같은 2가지 종류로 나뉩니다.
- 공유 Lock (Shared Lock, Read Lock, S-Lock)
- 배타 Lock (Exclusive Lock, Write Lock, X-Lock)
1-1. 공유 Lock (Shared Lock, Read Lock, S-Lock)
- 공유 Lock은 데이터를 변경하지 않는 읽기 작업을 위해 잠그는 것을 말합니다.
- 특정 데이터에 공유 Lock을 건다고 했을 때, 다음과 같은 특징을 가집니다.
- 하나의 세션에서 읽기 작업을 수행할 때, 다른 세션에서 해당 데이터를 읽어도 데이터의 정합성은 지켜지기 때문에 다른 세션의 공유 Lock을 막을 이유가 없다.
- 하나의 세션에서 읽기 작업을 수행할 때, 다른 세션에서 해당 데이터에 쓰기 작업을 수행한다면 기존 세션의 작업 결과가 달라질 수 있기 때문에 데이터 정합성이 지켜지지 않으므로 다른 세션의 배타 Lock 획득은 막는다.
- 다른 세션에서 해당 데이터에 공유 Lock을 걸고 접근할 수 있다.
- 다른 세션에서 해당 데이터에 배타 Lock을 걸고 접근할 수 없다.
요약하면, ‘세션 A가 공유 Lock을 획득했다.’의 의미는 다음과 같습니다.
세션 A가 특정 데이터를 읽기 위해 공유 Lock을 획득했고, 다른 세션 B도 같은 데이터를 공유 Lock을 걸고 읽을 수 있지만 어떤 세션도 해당 데이터를 배타 락을 걸고 수정할 수 없다.
“내가 데이터 읽는 동안 읽을 수는 있는데, 해당 데이터 변경하려고 선점하지 마라(X-Lock 걸지 마라)”
1-2. 배타 Lock (Exclusive Lock, Write Lock, X-Lock)
- 배타 Lock은 데이터를 변경하는 작업을 위해 잠그는 것을 말합니다.
- 특정 데이터에 배타 Lock을 건다고 했을 때, 다음과 같은 특징을 가집니다.
- 하나의 세션에서 쓰기 작업을 수행할 때, 다른 세션에서 해당 데이터를 읽는다면 작업 결과가 달라질 수 있기 때문에 데이터 정합성이 지켜지지 않으므로 다른 세션의 공유 Lock 획득은 막는다.
- 하나의 세션에서 쓰기 작업을 수행할 때, 다른 세션에서 해당 데이터 쓰기 작업을 한다면 기존 쓰기 작업 결과가 달라질 수 있기 때문에 데이터 정합성이 지켜지지 않으므로 다른 세션의 배타 Lock 획득은 막는다.
- 다른 세션에서 해당 데이터에 공유 Lock을 걸고 접근할 수 없다.
- 다른 세션에서 해당 데이터에 배타 Lock을 걸고 접근할 수 없다.
요약하면, ‘세션 A가 배타 Lock을 획득했다.’의 의미는 다음과 같습니다.
세션 A가 특정 데이터를 쓰기 위해 배타 Lock을 획득했고, 어떤 세션도 해당 데이터를 읽기 위해 공유 락을 걸고 읽는 것이 불가능하고, 쓰기 위해 배타 락을 걸고 쓰는 것이 불가능하다.
“내가 데이터 쓰는 동안 해당 데이터 읽거나 변경하려고 선점하지 마라! (S-Lock, X-Lock 걸지 마라)”
2. 블로킹 (Blocking)
보통 DB 작업을 수행할 때 데이터의 무결성과 정합성을 보장하기 위해 트랜잭션을 사용합니다.
따라서 Lock도 하나의 트랜잭션 안에서 걸리고, 해제되게 됩니다.
블로킹은 Lock 간의 경합이 발생해서 특정 트랜잭션이 작업을 진행하지 못하고 대기하는 상태를 의미합니다.
위의 공유 Lock, 배타 Lock 설명에서 블로킹 상태를 다음과 같이 언급했었습니다.
- 특정 데이터에 공유 Lock이 설정된 상태에서 해당 데이터에 배타 Lock을 설정하려고 할 때
- 특정 데이터에 배타 Lock이 설정된 상태에서 해당 데이터에 공유 Lock을 설정하려고 할 때
- 특정 데이터에 배타 Lock이 설정된 상태에서 해당 데이터에 공유 Lock을 설정하려고 할 때
아래의 그림은 블로킹 상황을 그림으로 표현한 예시입니다.
Coupon 데이터에 트랜잭션 A가 S-Lock을 설정한 후에 트랜잭션 B가 X-Lock을 설정하여
트랜잭션 B가 블로킹 상태에 진입한 것을 알 수 있습니다.
2-1. 블로킹 상태 예시
그렇다면, 블로킹 상태를 직접 체험해봅시다.
앞으로 설명할 예시들은 MySQL 8.0의 InnoDB 엔진을 사용한 예시입니다.
앞서 설명했던 블로킹 상태에 진입하는 경우에 먼저 Lock을 설정한 트랜잭션이 커밋되기 전에
이후에 Lock을 설정하는 트랜잭션의 작업은 ‘블로킹’ 상태에 진입하여 대기합니다.
위는 서로 다른 2개 세션에서의 블로킹 상태 예시를 나타낸 것입니다.
왼쪽 세션에서 트랜잭션 시작 후에 공유 Lock을 설정해놓은 후에
오른쪽 세션에서 트랜잭션을 시작하고 배타 Lock을 설정하려고 할 때,
블로킹 상태에 진입해서 작업이 수행되지 않고 대기하는 것을 볼 수 있습니다.
블로킹 상태의 이후에 Lock을 설정하는 트랜잭션의 작업이 정상적으로 수행되기 위해서는
Lock의 Timeout 시간 이전에 기존 Lock을 설정한 트랜잭션이 commit되거나 rollback 되는 방법밖에 없습니다.
위의 예시를 보면 앞의 예시와 똑같이 오른쪽 세션의 트랜잭션을 시작하고 배타 Lock을 설정하여 블로킹 상태에 진입해서 대기했지만,
왼쪽 세션의 먼저 공유 Lock을 설정한 트랜잭션을 commit하여 공유 Lock이 해제되었기 때문에
오른쪽 세션의 트랜잭션이 블로킹 상태에서 빠져나와서 작업을 수행한 것을 볼 수 있습니다.
만약 정해진 Lock Timeout 시간 이전에 트랜잭션이 종료되지 않으면
다음과 같은 Lock Timeout 관련 ERROR가 발생하고 트랜잭션이 롤백되어 종료됩니다.
2-2. 블로킹 상태 시 해결방안
이렇듯, 동시성 관련해서 데이터 정합성을 보장하기 위해 Lock을 설정할 때 블로킹 상태를 고려하여 설정해야 합니다.
블로킹 상태가 길어지면 그동안 애플리케이션 서비스에서 해당 데이터를 사용하는 작업이 모두 지연될 것이기 때문에
이러한 블로킹 상태를 해결하는 것이 중요합니다.
해결방안은 다음과 같습니다.
트랜잭션 작업 단위를 최대한 적게 구성하기
- 트랜잭션의 작업 단위를 적게 구성하면 그만큼 빠르게 트랜잭션이 종료되기 때문에 블로킹 상태 해결이 빠를 것입니다.
- JPA 사용 시 생명주기가 다른 객체간의 직접 참조를 간접 참조로 끊는 방법도 트랜잭션 작업 단위를 적게 구성하여 블로킹 상태를 빠르게 해결하는 예시로 들 수 있을 것 같습니다.
동일한 데이터를 동시에 변경하는 작업을 하지 않도록 설계하기
- 이런 부분은 동시성을 해결하는 비관적 락, 낙관적 락을 사용하여 해결할 수 있을 것 같습니다.
트랜잭션이 활발한 주간에는 대용량 데이터 작업 수행을 지양하기
- 서비스의 피크 타임에 서버의 스케쥴링을 수행하면 블로킹 상태가 길어져 사용자의 불편을 초래할 수 있을 것 같습니다.
- 만약 피크 타임에 어쩔 수 없이 대용량 데이터 작업을 수행해야 한다면, 작업 단위를 쪼개서 실행하는 것이 좋을 것 같습니다.
3. 데드락 (Dead Lock)
데드락은 두 트랜잭션 모두가 블로킹 상태에 진입하여 서로의 블로킹을 해결할 수 없는 상태입니다.
트랜잭션 A, B에서 A가 B 트랜잭션에 대해 블로킹 상태로 진입한 경우에
B 트랜잭션이 종료(커밋 or 롤백)되어야 해당 블로킹이 끝나고 트랜잭션 A의 작업이 정상적으로 수행됩니다.
그러나 해당 상황의 B 트랜잭션에서 A에 대해 블로킹 상태로 진입한다면 마찬가지로
A 트랜잭션이 종료(커밋 or 롤백)되어야 해당 블로킹이 끝나고 트랜잭션 B의 작업이 정상적으로 수행됩니다.
이때 A와 B가 모두 상대 트랜잭션의 종료를 기다리고 있게 되어 서로의 블로킹을 영원히 해결할 수 없는 상태가 됩니다.
이러한 상황을 데드락 (Dead Lock)이라고 합니다.
위는 데드락 상황을 그림으로 나타낸 것입니다.
간략하게 과정을 설명하면 다음과 같습니다.
- 세션 A에서 Coupon 데이터에 X-Lock 설정 & 세션 B에서 Member 데이터에 X-Lock 설정
- 세션 A에서 X-Lock 설정이 되어있는 Member 데이터에 S-Lock을 설정
- Member 데이터에는 이미 X-Lock 설정이 되어 있으므로 트랜잭션 A는 블로킹 상태 진입
- 세션 B에서도 X-Lock 설정이 되어 있는 Coupon 데이터에 S-Lock 설정
- Coupon 데이터에는 이미 X-Lock 설정이 되어 있으므로 트랜잭션 B는 블로킹 상태 진입
- 트랜잭션 A, B의 블로킹 상태는 상대 트랜잭션이 종료되어야 해결되는데 서로의 트랜잭션이 블로킹 상태이기 때문에 종료되지 않으므로 데드락 상태가 됩니다.
3-1. 데드락 상태 예시 (데드락 감지 off)
그렇다면 위와 같은 블로킹 상태를 직접 체험해봅시다.
앞으로 설명할 예시들은 MySQL 8.0의 InnoDB 엔진을 사용한 예시입니다.
MySQL에서는 데드락을 자동으로 감지하여 데드락 상황이 되면 하나의 트랜잭션을 롤백하고 종료하여 데드락을 해결합니다.
저는 데드락 상황을 유발하고 싶었기 때문에 MySQL의 데드락 감지 옵션을 다음과 같이 해제했습니다.
set global innodb_deadlock_detect=off;
위의 예시에서 왼쪽 세션을 세션 A, 오른쪽 세션을 세션 B로 하여 데드락 상황을 유발했습니다.
결과적으로, MySQL의 lock timeout 시간이 지나면 MySQL에서 Lock Timeout 관련 ERROR가 발생하고
트랜잭션이 롤백되어 종료되어 데드락이 해결되는 것을 알 수 있었습니다.
3-2. 데드락 상태 예시 (데드락 감지 On - Default)
그렇다면 MySQL의 데드락 감지를 사용해봅시다.
세션 B에서 데드락 감지 에러가 발생하면서 트랜잭션 B가 자동으로 롤백되어 종료되고
그로 인해 세션 A의 블로킹 상태가 해제되어 정상적으로 S-Lock 작업이 수행된 것을 볼 수 있습니다.
Reference
'DB' 카테고리의 다른 글
[DB] MySQL InnoDB의 인덱스(feat. 클러스터링 인덱스, 세컨더리 인덱스, 인덱스 스캔 종류, 다중 컬럼 인덱스) (1) | 2023.11.13 |
---|---|
[DB] DB 인덱스(Index)란? (1) | 2023.11.12 |
[DB] 트랜잭션 격리 수준 알아보기 (2) | 2023.09.29 |
[DB] 트랜잭션이란? (feat. ACID 특성) (2) | 2023.04.18 |
[DB] 친구 테이블 ERD 설계, 친구 목록 조회 기능 구현(Querydsl 셀프 조인) (1) | 2023.02.01 |