테마
한빛유통 쇼핑몰은 매시 정각에 「구매 적립금 지급」 배치를 돌린다. 직전 한 시간에 결제가 끝난 주문마다 고객 등급으로 적립금을 계산해 포인트 시스템에 보내고, 주문에 지급 표시를 한다. 지난달 '지급하는 사이에 취소된 주문에도 적립금이 나갔다'는 사고가 나서 개발자가 커서에 FOR UPDATE를 붙였다.
그 뒤로 고객센터의 「고객 정보 수정」과 쇼핑몰의 「배송지 변경」이 매시 정각부터 2분 남짓 저장 버튼에서 멈춘다. 개발자의 말이다. "배치는 고객 테이블을 한 줄도 안 고쳐요."
T_ORD 주문: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ) · IX_T_ORD_PAY (PAY_DT)(이 문제 설정) · 지급 표시 PNT_YN(문제 설정, 기본 'N') · 한 시간에 결제가 끝나는 주문은 400건 안팎M_CUS 고객: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · CUS_GD (A 1% · B 9% · C 90%) · 연락처 TEL_NO와 배송지 ADDR(문제 설정) · 고객센터와 쇼핑몰은 연락처나 배송지를 고칠 때 고객 행을 UPDATE한다T_PNT_HIS 적립금 지급 이력(이 문제의 파생 테이블) · 포인트 시스템은 같은 주문 번호로 두 번 온 지급 요청을 한 번만 처리한다-- 「구매 적립금 지급」 배치 (매시 정각) · 직전 한 시간에 결제가 끝난 주문
DECLARE
CURSOR c IS
SELECT o.ORD_SEQ, o.CUS_ID, o.PAY_AMT, c.CUS_GD
FROM T_ORD o, M_CUS c
WHERE c.CUS_ID = o.CUS_ID
AND o.PAY_DT >= :from_dt AND o.PAY_DT < :to_dt
AND o.ORD_ST = 'COMPLETE'
AND o.PNT_YN = 'N'
FOR UPDATE; -- 지급하는 사이 주문이 취소되지 않게 (지난달 추가)
v_pnt NUMBER;
BEGIN
FOR r IN c LOOP
v_pnt := ROUND(r.PAY_AMT * CASE r.CUS_GD WHEN 'A' THEN 0.03 WHEN 'B' THEN 0.02 ELSE 0.01 END);
PKG_PNT.SEND(r.CUS_ID, r.ORD_SEQ, v_pnt); -- 포인트 시스템 (건당 0.3초)
INSERT INTO T_PNT_HIS (ORD_SEQ, CUS_ID, PNT_AMT, REG_DT) VALUES (r.ORD_SEQ, r.CUS_ID, v_pnt, SYSDATE);
UPDATE T_ORD SET PNT_YN = 'Y' WHERE ORD_SEQ = r.ORD_SEQ;
END LOOP;
COMMIT;
END;
[커서 · ALLSTATS LAST · 14:00 배치 (직전 한 시간에 결제가 끝난 주문 418건)]
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 418 |00:00:00.02 | 876 |
| 1 | FOR UPDATE | | 1 | | 418 |00:00:00.02 | 876 |
|* 2 | HASH JOIN | | 1 | 410 | 418 |00:00:00.01 | 38 |
|* 3 | TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD | 1 | 410 | 418 |00:00:00.01 | 8 |
|* 4 | INDEX RANGE SCAN | IX_T_ORD_PAY | 1 | 436 | 436 |00:00:00.01 | 3 |
| 5 | TABLE ACCESS FULL | M_CUS | 1 | 2000 | 2000 |00:00:00.01 | 30 |
-------------------------------------------------------------------------------------------------------------
2 - access("C"."CUS_ID"="O"."CUS_ID")
3 - filter(("O"."ORD_ST"='COMPLETE' AND "O"."PNT_YN"='N'))
4 - access("O"."PAY_DT">=:FROM_DT AND "O"."PAY_DT"<:TO_DT)
[시간표 · 14:00 배치]
시각 적립금 배치 (SID 47) 고객 정보 화면
14:00:00 커서 OPEN · 주문 418행과 그 주문의 고객 401명 행을 잠금
14:00:01 ~ 주문마다 포인트 시스템 호출 · 지급 이력 INSERT · PNT_YN 고침
14:00:40 고객센터 (SID 133) CUS_000731 연락처 저장 → 기다림 (SID 47)
14:01:15 쇼핑몰 (SID 152 · 160) 배송지 변경 → 기다림 (SID 47)
14:02:06 COMMIT 모두 풀림
[V$SESSION · 14:01:30]
SID EVENT SECS BLOCKING_SESSION 하고 있는 문장
47 TCP Socket (KGAS) 0 PKG_PNT.SEND (포인트 시스템 응답 대기)
133 enq: TX - row lock contention 50 47 UPDATE M_CUS SET TEL_NO = :1, ADDR = :2 WHERE CUS_ID = :3
152 enq: TX - row lock contention 15 47 UPDATE M_CUS SET ADDR = :1 WHERE CUS_ID = :2
160 enq: TX - row lock contention 12 47 (같은 문장)
[V$LOCK · 14:01:30 (TM 은 객체 이름으로)]
SID TYPE 대상 LMODE REQUEST BLOCK
47 TM T_ORD 3 0 0
47 TM M_CUS 3 0 0 ← 배치는 M_CUS 를 한 행도 고치지 않는다
47 TM T_PNT_HIS 3 0 0
47 TX 배치의 트랜잭션 6 0 1
133 TM M_CUS 3 0 0
133 TX 배치의 트랜잭션 0 6 0
[고객 정보 저장 · 지난주 애플리케이션 로그]
매시 00 ~ 02분 평균 38초 · 그 밖의 시간 0.02초 · 기다린 고객은 모두 직전 한 시간에 결제한 고객
V$LOCK을 근거로 배치가 고치지도 않는 고객 행을 왜 잠그는지 설명하시오.FOR UPDATE 줄의 Buffers가 어떻게 바뀌는지도 쓰시오.