기타

Conditional Atomic Update 동시성 제어

gw1 2025. 7. 13. 14:23

목표 : 재고 음수가 되지 않으면서(정합성) 기존보다 동시성(성능)이 떨어지면 안됨
 
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