테마
버거원 본사는 신메뉴 출시에 맞춰 「더블 패티 세트」를 전국 하루 5,000개 한정으로 팔았다. POS는 세트를 담을 때마다 본사 DB의 남은 수량을 확인하고 담은 만큼 줄인다. 12:41에 남은 수량이 0이 되어 판매가 멈췄는데, 마감 뒤 주문상품을 세어 보니 5,037개가 팔려 있었다. 재료가 모자란 매장 37곳은 고객에게 쿠폰으로 사과했다. 개발팀은 이렇게 말했다. "UPDATE가 행 락을 기다리게 되어 있으니 동시에 줄여도 안전합니다."
ITEM_LMT 한정 판매 수량(이 문제를 위해 더한 테이블): PK (ITEM_ID, SALE_DT) · LMT_QTY 한정 수량 · REMAIN_QTY 남은 수량 · 한정 메뉴 하루 한 행ITEM 상품: 92행 · 5블록 · 더블 패티 세트는 ITEM_ID = 'I0088'ORD_ITEM 주문상품: 2,472,717행 · ORD_ITEM_PK (ORD_NO, ITEM_ID)ORD와 ORD_ITEM INSERT, 카드 승인(VAN 응답 평균 1.1초), COMMIT-- PKG_POS.ADD_LIMITED — 한정 세트를 담을 때 (같은 트랜잭션에서 주문을 넣고, 카드 승인 뒤 COMMIT)
SELECT REMAIN_QTY INTO v_remain
FROM ITEM_LMT
WHERE ITEM_ID = :item_id AND SALE_DT = :sale_dt;
IF v_remain < :qty THEN
RAISE_APPLICATION_ERROR(-20010, '한정 수량이 모두 팔렸습니다');
END IF;
UPDATE ITEM_LMT
SET REMAIN_QTY = v_remain - :qty
WHERE ITEM_ID = :item_id AND SALE_DT = :sale_dt;
[시간표 — 12:31:07 · 남은 수량 412]
시각 매장 SH0031 POS (SID 74) 매장 SH0158 POS (SID 191)
12:31:07.10 SELECT → 412 · 412 - 1 = 411
UPDATE … SET REMAIN_QTY = 411 · 주문 INSERT · 카드 승인 요청
12:31:07.30 SELECT → 412 (74 는 COMMIT 전이라 412 가 보인다) · 411 계산
UPDATE … SET REMAIN_QTY = 411 → 기다림
12:31:08.21 승인 · COMMIT (REMAIN_QTY 411) → 깨어남 · 계산해 둔 411 을 그대로 쓴다
12:31:09.40 승인 · COMMIT (REMAIN_QTY 411)
→ 두 개가 팔렸는데 남은 수량은 하나만 줄었다
[V$SESSION — 12:31:07.8]
SID EVENT SECS BLOCKING_SESSION 하는 문장
74 SQL*Net message from client 0 (카드 승인 응답 대기 · 트랜잭션을 연 채)
191 enq: TX - row lock contention 0 74 UPDATE ITEM_LMT SET REMAIN_QTY = :B3 - :B2 …
[마감 집계]
한정 5,000 · 판매(ORD_ITEM 집계) 5,037 · 마지막 REMAIN_QTY 0
12:00 ~ 12:41 enq: TX - row lock contention (ITEM_LMT) 1,906번 · 평균 0.7초
기다렸다 깨어난 UPDATE 가운데 앞사람이 줄인 값을 덮어쓴 경우 37번