주문 처리에서 중간에 실패하면 ROLLBACK 으로 전부 물러나게 만들었다. 실패하는 경우를 시험해 보니 일부만 물러났다.
Table of contents
Open Table of contents
안쪽에서도 트랜잭션을 쓰고 있었다
바깥 코드는 세 가지를 하나로 묶고 있었다.
$this->db->trans_start();
$orderNo = $this->order_model->create($data);
$this->stock_model->decrease($items);
$this->point_model->use($userId, $point);
$this->db->trans_complete();
재고를 줄이는 모델 안을 열어 보니 거기서도 trans_begin 과 trans_commit 을 직접 부르고 있었다.
원인은 안쪽이 끝내서가 아니라 안쪽이 여는 순간이었다. MySQL 은 트랜잭션을 겹칠 수 없고 열려 있는 상태에서 START TRANSACTION 을 다시 내면 그 시점에 앞의 것이 암묵적으로 커밋된다. 매뉴얼도 트랜잭션은 중첩될 수 없으며 그것이 START TRANSACTION 의 암묵 커밋에서 따라 나오는 결과라고 적어 뒀다.
그러니 재고 모델에 들어가는 순간 주문 생성이 확정된다. 그 뒤에 실패해도 앞의 것은 안 물러난다.
여는 자리를 한 곳으로 정했다
트랜잭션을 여는 계층을 컨트롤러로 정하고 모델에서는 trans_begin 을 쓰지 않게 했다. 모델 하나만 쓰는 경우에도 컨트롤러가 연다. 규칙이 하나면 어느 자리에서 열려 있는지 따질 일이 없다.
public function decrease($items) {
foreach ($items as $i) { ... } // 트랜잭션 없음
}
trans_start 와 trans_complete 를 쓰면 프레임워크가 깊이를 세서 가장 바깥에서만 실제로 확정한다. DB 쪽에서 중첩이 되는 것이 아니라 드라이버가 안쪽 호출을 세기만 하고 넘기는 것이다. 그것도 되지만 어느 깊이인지를 계속 추적해야 해서 여는 자리를 정하는 쪽이 단순했다.
되돌아가는지 확인했다
고친 뒤 일부러 실패시켜 봤다. 포인트 차감 직후에 예외를 던지고 세 테이블을 확인했다.
SELECT COUNT(*) FROM orders WHERE order_no = ?; -- 0
SELECT qty FROM stock WHERE product_no = ?; -- 원래 값
SELECT point FROM member WHERE member_no = ?; -- 원래 값
셋 다 원래대로였다. ROLLBACK 은 일부러 실패시켜 보기 전에는 확인이 안 된다. 정상 경로만 돌리면 그 코드는 한 번도 안 돈다.
트랜잭션 밖의 것
물러나지 않는 것도 있었다. move_uploaded_file 과 curl_exec 과 mail() 과 memcached 갱신 넷은 데이터베이스 밖이라 ROLLBACK 이 돌아도 그대로 남는다.
순서를 바꿔서 확정된 뒤에 하게 했다.
$this->db->trans_start();
// 데이터베이스 작업만
$this->db->trans_complete();
if ($this->db->trans_status() === false) {
return $this->fail();
}
$this->mailer->sendOrderConfirm($orderNo);
$this->cache->delete("product:{$productNo}");
이러면 물릴 일이 없다. 밖의 것이 실패해도 주문은 남고 그건 따로 재시도한다.
오래 걸리는 것을 트랜잭션 안에 두지 않는 것도 같이 정했다. curl_exec 을 안에서 부르면 CURLOPT_TIMEOUT 만큼 기다리는 동안 InnoDB 의 행 잠금이 유지되고 다른 요청이 밀린다. 결제 승인을 트랜잭션 밖으로 빼고 그 결과를 받은 뒤에 짧게 열어 저장했다.
정리
- MySQL 은 트랜잭션을 겹칠 수 없다
- 안쪽에서
START TRANSACTION을 내면 그 순간 바깥 것이 커밋된다 - 여는 자리를 한 계층으로 정한다. 모델은 안 열고 컨트롤러가 연다
trans_start는 드라이버가 깊이를 세는 것이지 DB 가 중첩하는 것이 아니다- 일부러 실패시켜
ROLLBACK이 도는지 확인한다 move_uploaded_file과curl_exec과mail()과 캐시는 안 물러난다- 그런 것은 확정된 뒤에 하고 실패하면 따로 재시도한다
curl_exec을 트랜잭션 안에 두면 행 잠금이 그만큼 길어진다