Skip to content
isdnetworks
Go back

연말 정산은 한 번만 돌아야 했다

한 해 실적을 정리하는 작업을 맡았다. 집계를 내고 등급을 부여하고 포인트를 지급하는 세 가지를 한 번에 도는 배치였다. 앞의 둘은 틀리면 다시 계산하면 되는데 포인트 지급이 걸렸다.

Table of contents

Open Table of contents

무를 수 없는 단계를 갈랐다

전에 배치를 두 번 돌려 집계가 두 배 된 적이 있었는데 그때는 REPLACE 로 덮어써서 풀었다. 이번에는 그 방법이 안 통한다. 집계는 다시 계산하면 같은 값이 나오지만 포인트는 이미 쓴 사람이 있을 수 있어 회수가 어렵다.

그래서 배치를 세 단계로 쪼갰다. 집계와 지급 대상 목록 만들기까지는 몇 번을 다시 돌려도 되고 실제 지급만 한 번 돌아야 한다. 조심할 자리를 하나로 줄인 것이다.

그 한 번을 지키는 것도 따로 필요했다. crontab 이 겹쳐 뜨거나 사람이 손으로 한 번 더 돌리면 그만이다. 지급 단계 시작에 flock 으로 잠금 파일을 잡게 했다. LOCK_EXLOCK_NB 를 함께 줘야 두 번째 프로세스가 기다리지 않고 바로 false 를 받는다. LOCK_NB 를 빼면 앞의 것이 끝나기를 기다렸다가 이어서 도니 막는 의미가 없다.

먼저 앞의 두 단계만 돌려서 COUNT(*) 로 대상이 1,234명이고 SUM(points) 가 얼마인지를 눈으로 봤다. 목록을 보니 탈퇴한 회원이 섞여 있었고 등급이 이상한 사람도 있었다. WHERE 를 고치고 두 번째 단계를 다시 돌리기를 여러 번 했다. 세 단계를 한 번에 돌았다면 잘못된 목록 그대로 지급이 나갔을 것이다.

대상 목록을 테이블에 남겼다

두 번째 단계의 결과를 화면에 찍고 마는 대신 payout_plan 테이블에 저장했다.

payout_plan
  user_id
  points
  status    wait / done

지급 단계는 payout_plan 만 본다.

foreach ($plan as $row) {
    if ($row->status === 'done') continue;
    지급($row);
    $row->status = 'done';
}

이미 준 것은 건너뛰므로 중간에 죽어도 이어서 돌릴 수 있다. 500명까지 주고 죽었으면 거기까지 statusdone 이고 다시 돌리면 501번부터 시작한다. payout_plan 을 안 남겼다면 어디까지 줬는지를 error_log 에서 세어야 했다.

표시를 먼저 하기로 했다

여기서 순서가 걸렸다. 주고 나서 표시하면 주고 죽었을 때 또 주게 되고, 표시하고 주면 표시한 뒤 죽었을 때 안 준 것이 준 것으로 남는다. 둘 중 하나는 반드시 틀린다.

표시를 먼저 하는 쪽을 골랐다. 안 준 것은 나중에 찾아서 줄 수 있지만 두 번 준 것은 회수가 어렵기 때문이다. 덜 주는 쪽이 덜 나쁘다는 판단이었다.

대신 안 준 것이 있는지 찾을 방법이 필요해졌다. payout_log 를 따로 남기고 NOT EXISTSpayout_plan 과 대조하게 했다.

SELECT p.user_id FROM payout_plan p
 WHERE p.status = 'done'
   AND NOT EXISTS (SELECT 1 FROM payout_log l WHERE l.user_id = p.user_id);

statusdone 인데 payout_log 가 없는 것이 안 준 것이다. 돌리고 확인하니 0이었다.

다만 0 이 나왔다고 이 쿼리가 제대로 도는지는 모른다. 시험 자료에서 payout_log 한 행을 일부러 빼고 돌려 그 회원이 잡히는 것을 먼저 봤다. 그러고 나서 본 0 이라야 0 이다.

돌리기 전에 만들어 둔 것

확인 쿼리 말고도 돌리기 전에 해 둔 것이 몇 가지 있다.

회원 등급과 포인트 잔액 테이블을 mysqldump 로 떴다. 결과적으로 쓰지 않았지만 받는 비용이 작고 안 받았을 때의 대가가 크다. 안 쓰면 그만인 쪽을 골랐다.

돌리는 시각도 정했다. 낮에는 사람이 많아 부하가 걸리므로 새벽으로 잡았다. 정말 그 시각이 한가한지 확인하고 싶었는데 그때는 서버 부하를 시간대별로 보는 방법을 몰라서 access_log 를 시간대별로 세어 대신 봤다.

시작할 때와 끝날 때 mail() 로 알림을 보내게 했다. 시작에는 대상 인원을, 완료에는 성공과 실패 수를 함께 넣었다. 완료라는 말만 오면 다 됐는지 알 수 없고, 실패 수가 있어야 보고 있지 않아도 결과를 안다.

정리


Share this post on:

Previous Post
이력 문장이 제각각이라 읽기 어려웠다
Next Post
넉 달 이어진 문제가 근거가 됐다