Skip to content
isdnetworks
Go back

지우고 다시 만드는 중에 멈추면 더 나빠졌다

집계 결과를 갱신할 때 지우고 다시 넣고 있었다. 중간에 멈춘 날 화면이 통째로 비었다.

Table of contents

Open Table of contents

증상 — 지우고 넣고 있었다

갱신하는 자리는 두 줄이었다.

DELETE FROM stat_daily WHERE stat_date = '2018-09-27';
INSERT INTO stat_daily SELECT ... ;

DELETEINSERT 두 줄이라 간단하고 다시 돌려도 결과가 같아 보였다.

문제는 DELETEINSERT 전에 멈추면 아무것도 없다는 것이었다. stat_daily 의 갱신에 실패했는데 갱신하기 전 값까지 함께 잃은 것이다.

성공하는 경로만 보고 만들면 실패 경로가 그대로 방치된다. 실패했을 때 이전 상태가 유지되느냐가 이 종류 처리의 기준이었다.

조치 — 한 트랜잭션에 넣었다

먼저 두 쿼리를 한 트랜잭션으로 감쌌다.

$db->transaction(function () use ($date) {
    $db->query("DELETE FROM stat_daily WHERE stat_date = ?", [$date]);
    $db->query("INSERT INTO stat_daily SELECT ...", [$date]);
});

중간에 실패하면 ROLLBACK 되어 지운 것도 함께 되돌아간다.

이 한 줄로 소실 자체는 대부분 해결됐다. 그런데 집계 자체가 오래 걸려서 그동안 stat_daily 가 잠겨 있었고 화면에서 그 표를 읽는 것이 밀렸다.

설정 — 새로 만들고 바꿔치기

전체를 다시 만들 때는 방식을 아예 바꿨다.

CREATE TABLE stat_daily_new LIKE stat_daily;
INSERT INTO stat_daily_new SELECT ...;
RENAME TABLE stat_daily TO stat_daily_old, stat_daily_new TO stat_daily;
DROP TABLE stat_daily_old;

stat_daily_new 에 다 만들고 나서 이름만 바꾸는데 RENAME TABLE 은 순식간이다.

만드는 동안 옛 표가 그대로 서비스한다. 실패하면 새 표만 버리면 되고 읽는 쪽은 영향을 안 받는다.

RENAME TABLE 에 둘을 같이 쓴 것은 그 사이에 stat_daily 라는 이름이 없는 순간을 만들지 않기 위해서였다.

선택지 — 일부만 갱신하는 경우

하루치만 갱신할 때는 표를 통째로 바꿀 수 없었다.

INSERT INTO stat_daily (stat_date, ...)
VALUES (?, ...)
ON DUPLICATE KEY UPDATE order_count = VALUES(order_count), amount = VALUES(amount);

ON DUPLICATE KEY UPDATE 로 덮어쓰므로 실패해도 원래 값이 남는다.

유일 인덱스가 있어야 동작한다.

ALTER TABLE stat_daily ADD UNIQUE KEY uk_date (stat_date);

uk_date 로 어떤 값이 같으면 같은 행인지가 정의된다. 중간에 멈춰도 처리된 날짜까지는 갱신되고 나머지는 옛 값이 그대로 남는다.

주의 — 이상한 값을 거르는 검사

덮어쓰기 전에 새 값이 말이 되는지 봤다.

$new = $this->calculate($date);
$old = $this->repo->get($date);

if ($old && $new['order_count'] < $old['order_count'] * 0.5) {
    log_error("집계 결과가 이전의 절반 미만입니다. 갱신하지 않습니다.");
    notify("집계 확인 필요: {$date}");
    return;
}
$this->repo->upsert($date, $new);

order_count 가 갑자기 절반 아래로 줄면 원본에 문제가 있을 수 있어 upsert 를 건너뛴다.

실제로 한 번 걸렸는데 원본 표의 일부가 아직 안 들어와 있었다. 그대로 덮었으면 옛 값도 함께 잃었을 것이다.

바꾸기 전 값도 남기게 했다.

INSERT INTO stat_daily_history (stat_date, order_count, amount, replaced_at)
SELECT stat_date, order_count, amount, NOW() FROM stat_daily WHERE stat_date = ?;

stat_daily_historyreplaced_at 과 함께 바뀐 이력이 쌓인다.

지난주에 본 숫자와 다르다는 말이 나왔을 때 이것으로 확인했다. 원본이 나중에 정정돼서 집계가 바뀐 것이었고 이력이 있어 언제 무엇이 바뀌었는지 설명할 수 있었다.

검증 — 일부러 만든 중간 실패

바꾼 방식이 실제로 다른지 확인했다.

if (getenv('FAIL_AFTER_DELETE')) { throw new RuntimeException('시험용 실패'); }

FAIL_AFTER_DELETE 를 걸어 지운 직후에 실패하게 만들었다.

옛 방식에서는 그 자리에서 데이터가 없어졌고 바꾼 방식에서는 옛 값이 그대로 있었다. 이것을 안 해 봤으면 트랜잭션이 제대로 걸렸는지도 몰랐을 것이다.

정리


Share this post on:

Previous Post
비슷해 보이는 엔드포인트 셋
Next Post
컨테이너에서 뭘 백업하나