목표 : 재고 음수가 되지 않으면서(정합성) 기존보다 동시성(성능)이 떨어지면 안됨
DB 상의 정합성을 제어하기 위해선 Lock이 필요한데 명시적으로 Lock을 걸면 동시성이 떨어짐
정합성 ↑ 이면 동시성 ↓
정합성 ↓ 이면 동시성 ↑
비관적 락(동시성↓) : SELECT FOR UPDATE → select 시 commit까지 레코드 락을 걸어 커밋까지 레코드를 독점함
낙관적 락 (동시성↓) : 명시적 락 없이 Version 칼럼으로 충돌 감지를 하여 재시도를 반복
→ 쓰레드가 죽지 않고 살아 있어 커넥션 점유 시간 증가, 커넥션 부족, Context Switch 시간 증가
그외에 고려 사항으로 기존 코드 및 시스템을 최대한 건드리지 않아야 함
하지만, 선착순 재고처럼 위 정합성과 동시성이 모두 필요한 경우
UPDATE문 자체의 배타적 X락(레코드 락)을 활용하면서
조건부 업데이트(Conditional Atomic Update) 및 명시적인 트랜잭션 롤백을 활용
※ 주의 사항 1 : MySQL 8.0 innoDB 기준임 DBMS마다 Locking 동작이 다를 수 있음
※ 주의 사항 2 : UPDATE문에 WHERE 재고 > 0이 핵심임
UPDATE 행이 0개인 경우 명시적으로 트랜잭션 강제 롤백을 선언하여야 함
※ 주의 사항 3 : 선 재고 차감이 아니라 후 재고 차감으로 구현되어야 함
UPDATE문을 최대한 트랜잭션 commit 직전에 하여 X락(레코드 락) 점유 시간을 줄여야 함
※ 주의 사항 4 : 해당 트랜잭션 격리 수준을 READ COMMITTED로 권장함
REPEATABLE READ는 UPDATE문 자체에 GAP LOCK(범위 락)이 걸려, 락 범위가 늘어나고 데드락 가능성이 존재함(하단 코드는 PK값이 unique하므로 해당 안됨)
※ 주의 사항 5 : MySQL innoDB의 레코드 락은 인덱스 상에 Lock을 걸으므로 사전에 product에 적절한 인덱스가 구성되어야 함
슈도 코드 버전
BEGIN TRANSACTION
재고 ← SELECT stock FROM product WHERE id = 상품번호
IF 재고 > 0 THEN
INSERT INTO temp_order (user_id, product_id)
영향행수 ← UPDATE product
SET stock = stock - 1
WHERE id = 상품번호 AND stock > 0
IF 영향행수 == 0 THEN
ROLLBACK
출력("재고 부족")
ELSE
COMMIT
출력("주문 성공")
ENDIF
ELSE
ROLLBACK
출력("재고 없음")
ENDIF
PHP 버전
<?php
// DB 연결
$conn = mysqli_connect("localhost", "user", "pass", "mydb");
if (!$conn) {
die("DB 연결 실패: " . mysqli_connect_error());
}
// 트랜잭션 시작
mysqli_begin_transaction($conn);
// 1. 재고 조회 (락 없이)
$product_id = 123;
$user_id = 456;
$sql = "SELECT stock FROM product WHERE id = $product_id";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_assoc($result);
if ($row && $row['stock'] > 0) {
// 2. 임시 주문 테이블에 삽입
$sql = "INSERT INTO temp_order (user_id, product_id) VALUES ($user_id, $product_id)";
if (!mysqli_query($conn, $sql)) {
mysqli_rollback($conn);
die("임시 주문 실패: " . mysqli_error($conn));
}
// 3. 조건부 재고 차감
$sql = "UPDATE product SET stock = stock - 1 WHERE id = $product_id AND stock > 0";
mysqli_query($conn, $sql);
if (mysqli_affected_rows($conn) === 0) {
mysqli_rollback($conn);
echo "재고 부족";
} else {
mysqli_commit($conn);
echo "주문 성공";
}
} else {
mysqli_rollback($conn);
echo "재고 없음";
}
// 연결 닫기
mysqli_close($conn);
?>
결론적으로 위와 같은 상황에는 조건부 업데이트(Conditional Atomic Update)을 활용하여 정합성과 동시성을 모두 챙길 수 있음
병렬 처리 후 마지막만 직렬화됨
비유 하자면 고속도로에서 차선은 많지만 톨게이트는 딱 1개 존재하는 느낌
하지만, 위 방식으로도 트래픽 감당이 안되면
DB로 해결이 불가능함
레디스 도입이 필요할 때
선재고차감+예외던져서 트랜잭션 탈출로 구현해도 됩니다.
'기타' 카테고리의 다른 글
| 백엔드 포트폴리오 주제 총 집합 (3) | 2025.08.05 |
|---|---|
| 프로젝트 리팩토링 및 DB 성능 개선 (2) | 2025.02.01 |