데이터베이스 시스템에서 동시성 제어는 핵심 기능 중 하나다. 여러 트랜잭션이 동시에 데이터에 접근할 때 데드락이 발생할 수 있는데, PostgreSQL은 이를 자동으로 감지하고 해결하는 메커니즘을 갖추고 있다.
데드락은 두 개 이상의 트랜잭션이 서로가 보유한 락을 기다리며 무한정 대기하는 상태를 말한다. 예를 들어 :
- 트랜잭션 A: 자원 1에 락을 건 상태에서 자원 2의 락을 기다림
- 트랜잭션 B: 자원 2에 락을 건 상태에서 자원 1의 락을 기다림
이런 상황에서 두 트랜잭션 모두 영원히 진행하지 못하는 교착 상태가 발생한다.
PostgreSQL은 다음과 같은 방식으로 데드락을 감지하고 처리한다:
deadlock_timeout 파라미터(기본값 1초) 이상 락을 기다리면 데드락 감지 프로세스가 실행된다.-- 데드락 감지 타임아웃 설정 변경 (밀리초 단위)
SET deadlock_timeout = '500ms';
ERROR: deadlock detected
DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5678.
Process 5678 waits for ShareLock on transaction 1234; blocked by process 1234.
HINT: See server log for query details.pg_stat_database 뷰를 통해 데이터베이스별 데드락 발생 통계를 확인할 수 있다.-- 데이터베이스별 데드락 통계 확인
SELECT datname, deadlocks FROM pg_stat_database;
PostgreSQL의 데드락 감지는 다음과 같은 단계로 이루어진다:
PostgreSQL의 소스 코드에서 데드락 감지 부분은 https://github.com/postgres/postgres/blob/master/src/backend/storage/lmgr/deadlock.c 에 구현되어 있다.
deadlock_timeout 값을 너무 낮게 설정하면 불필요한 데드락 검사로 성능이 저하될 수 있다.다음은 데드락이 발생할 수 있는 두 트랜잭션의 예시다:


세션 1:
BEGIN;
-- 먼저 hub_id '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'의 상품들을 업데이트 (1)
UPDATE public.p_stock
SET quantity = quantity - 10, updated_at = NOW(), updated_by = 10
WHERE hub_id = '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'
AND product_id = '8a47ecaa-141f-4ebf-885c-dcc0bcbc644c';
SELECT pg_sleep(2);
-- 다음으로 다른 hub에 있는 상품 업데이트 시도 (3)
UPDATE public.p_stock
SET quantity = quantity + 5, updated_at = NOW(), updated_by = 10
WHERE hub_id = '80f07400-d252-4130-abe3-99df408eadf1'
AND product_id = '2bbb56d6-9961-4ece-aa1e-e3851f47569e';
COMMIT;
세션 2:
-- 먼저 hub_id '80f07400-d252-4130-abe3-99df408eadf1'의 상품을 업데이트 (2)
UPDATE public.p_stock
SET quantity = quantity - 20, updated_at = NOW(), updated_by = 11
WHERE hub_id = '80f07400-d252-4130-abe3-99df408eadf1'
AND product_id = '2bbb56d6-9961-4ece-aa1e-e3851f47569e';
-- 잠시 대기 (다른 세션이 락을 획득할 시간) (2)
SELECT pg_sleep(2);
-- 다음으로 다른 hub에 있는 상품들 업데이트 시도 (4)
UPDATE public.p_stock
SET quantity = quantity + 15, updated_at = NOW(), updated_by = 11
WHERE hub_id = '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'
AND product_id IN ('8a47ecaa-141f-4ebf-885c-dcc0bcbc644c', 'f000f6f5-a4ad-47be-9b11-5c8e060eb5e4');
COMMIT;
이런 상황에서 PostgreSQL은 데드락을 감지하고 둘 중 하나의 트랜잭션을 희생자로 선택해 롤백한다.
-- 세션 2에서 실행된 내용:
-- 1. 첫 번째 UPDATE 쿼리: '80f07400-d252-4130-abe3-99df408eadf1' hub의 상품 수량 감소
-- 이 시점에서 해당 행에 대한 배타적 락(X Lock)을 획득함
product.public> UPDATE public.p_stock
SET quantity = quantity - 20, updated_at = NOW(), updated_by = 11
WHERE hub_id = '80f07400-d252-4130-abe3-99df408eadf1'
AND product_id = '2bbb56d6-9961-4ece-aa1e-e3851f47569e'
[2025-03-27 22:54:03] 1 row affected in 5 ms -- 성공적으로 1행 업데이트
-- 2. 2초 동안 대기 (이 시간 동안 세션 1이 다른 행에 락을 획득할 기회를 줌)
product.public> SELECT pg_sleep(2)
[2025-03-27 22:54:05] 1 row retrieved starting from 1 in 2 s 161 ms (execution: 2 s 11 ms, fetching: 150 ms)
-- 3. 두 번째 UPDATE 쿼리: '5e27b315-a4d4-43e2-81fd-e2f3771f46fc' hub의 상품 수량 증가
-- 이 시점에서 세션 1이 이미 이 행에 락을 보유하고 있음
-- 그리고 세션 1은 세션 2가 잠근 행에 접근을 시도하고 있음 -> 데드락 발생!
product.public> UPDATE public.p_stock
SET quantity = quantity + 15, updated_at = NOW(), updated_by = 11
WHERE hub_id = '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'
AND product_id IN ('8a47ecaa-141f-4ebf-885c-dcc0bcbc644c', 'f000f6f5-a4ad-47be-9b11-5c8e060eb5e4')
[2025-03-27 22:54:05] 2 rows affected in 5 ms -- 이 쿼리는 성공했지만 데드락 감지로 트랜잭션이 이미 롤백됨
-- 4. COMMIT 시도: 하지만 데드락으로 인해 이미 트랜잭션이 자동 롤백됨
product.public> COMMIT
[2025-03-27 22:54:05] [25P01] there is no transaction in progress -- 에러: 진행 중인 트랜잭션 없음
[2025-03-27 22:54:05] completed in 4 ms
ALTER SYSTEM SET log_min_messages = 'DEBUG5';
-- 데드락 관련 로그 활성화
ALTER SYSTEM SET log_lock_waits = on;
-- 변경사항 적용
SELECT pg_reload_conf();
[2025-03-27 23:03:17] [40P01] ERROR: deadlock detected
[2025-03-27 23:03:17] Detail: Process 57417 waits for ShareLock on transaction 275838; blocked by process 58881.
[2025-03-27 23:03:17] Process 58881 waits for ShareLock on transaction 275837; blocked by process 57417.
[2025-03-27 23:03:17] Hint: See server log for query details.
[2025-03-27 23:03:17] Where: while updating tuple (0,45) in relation "p_stock"
에러 코드: 40P01 - PostgreSQL의 데드락 감지 코드
상호 차단 구조:
발생 지점: p_stock 테이블의 튜플(0,45) 업데이트 과정
이런 상황은 서로 다른 행에 락을 잡은 두 트랜잭션이 각자 상대방이 잠근 행에 접근하려 할 때 발생한다. PostgreSQL의 데드락 감지기는 이 상황을 자동으로 감지하고 둘 중 하나의 트랜잭션을 희생자로 선택해 롤백시킨다.
- 각 프로세스가 서로 다른 행에 배타적 락을 획득
- 각 프로세스가 서로의 락을 요청하며 대기
- 데드락 감지기가 순환 대기 상태 감지
- 비용이 적은 트랜잭션을 자동 롤백
- 롤백된 트랜잭션은 "deadlock detected" 에러 반환
- 다른 트랜잭션은 정상 진행
PostgreSQL의 자동 데드락 감지 및 해결 메커니즘은 데이터 일관성을 유지하면서 시스템이 계속 작동할 수 있게 해주는 중요한 기능이다. 그러나 데드락 감지 자체는 이미 발생한 문제를 해결하는 방법
(탐지 및 복구)이므로, 애플리케이션 설계 단계에서 데드락을 예방(예방 및 회피)하는 전략을 함께、사용하는 것이 바람직하겠다.
매일 다른 기술 블로그 대단하세요! 잘 보고 있습니다!!