가위바위보 이벤트가 생각보다 잘되었고, 이걸 조금 더 개선해보는 아이디어 중 하나가 가위바위보 몇등을 했는지를 보여주는 리더보드를 만드는 것이었다. 랭킹을 보여주려면 레디스의 zSet을 쓰면 된다고 생각했다. 하지만 문제는 과거 데이터까지도 다 반영해서 랭킹을 보여줘야 한다는 것이었다.
특정 새로운 시점부터 랭킹을 매기기 시작한다면 그냥 레디스의 zSet을 쓰고 이길때마다 레디스에서 점수를 반영하면 되는데 과거 데이터를 반영해야해서 데이터 마이그레이션이 필요했다.
<비즈니스 요구사항>
같은 등수를 허용하지 않는다는데서 어려웠다.
score에는 몇 승 했는지 정보만 넣으려고 했기 때문이다. 고민을 하다가 메이트님의 아이디어로 쉽게 해결할 수 있었다. score를 'n승.userId' 로 넣기로 했다. userId는 유니크하기 때문에 이렇게 하면 같은 승수라 할지라도 같은 등수가 발생하지 않는다. 이 방식을 이용하면 다음에 생성 순서 같은 것도 millisec 을 소수점으로 적용시켜서 가능할 것 같았다.
아래와 같은 순서로 배포를 해야 빠지는 데이터 없이 모든 데이터를 반영해서 등수를 반영할 수 있다.
winner 테이블에 없는 경우
a. winner 테이블에 생성
a. 승리시 테이블에 1승 쌓이기
b. 레디스에 등수 쌓기 시작 (없으면 등록)
winner 테이블에 이미 있는 경우
- 리더보드 테이블에 존재하면 update(+1)
배치는 winner를 200개씩 winner id 순으로 끊어서 가져온다. winCount가 0 초과이면, 리더보드 테이블에 winCount를 넣어주는 방식이다.
이렇게 작업 순서를 구상했다. 이렇게만 흘러갔으면 좋았겠지만..
배치 작업 로직에 문제가 있었고, 중간에 실행되다가 그 로직 때문에 예외가 발생해서 처음부터 배치를 돌리게 되었다.
다시 db에 넣는데 unique 제약에 걸리면 안되기 때문에 INSERT IGNORE INTO
를 사용했다. 이걸 사용하면 없으면 넣고 있으면 아무 작업을 하지 않는 것이다.
또 당연히 batchUpdate를 사용했다.
이렇게 하니 200개를 가져와서 작업하고 200 밀리세컨씩 sleep을 줬음에도 1초에 200개정도씩 진행이 되어서 100만개를 금방할 수 있었다. 200개씩 가져오고 200 밀리세컨씩 sleep을 준 것은, 당시 프로메테우스로db 평균 qps를 보고 크게 벗어나지 않게 계산해서 결정한 값이다.
대다수의 유저들은 당연히 1승을 하고 만다(1승을 하면 리워드를 받기 때문에). 그래서 대략 40만 정도의 유저가 1승이었다. 제품 스펙에 '만약 1승을 더 한다면 몇등이 오를지'를 보여주는 것이 있었는데, 1승만 한 유저들은 1승을 하면 얼마나 오를지 카운팅할때 시간이 많이 걸리는 문제가 있었다. zrange를 사용해서 발생한 문제였다.