LabHub

Redis 와 캐싱 · 랭킹을 ZSet 으로 · 실습

리더보드를 ZSet 으로 구현하고 검증하기

LabHub 에서 이어서 보기

목표

정렬 집합 하나로 실시간 리더보드의 전체 요구사항 — 상위 N, 개인 순위, 페이지네이션, 동점 규칙, 기간별 랭킹, 합산 — 을 구현하고 조회 성능을 숫자로 확인한다.

왜 중요한가

랭킹은 관계형 DB 가 가장 못하는 일 중 하나입니다. 상위 10명은 인덱스로 쉽지만, "내 순위"는 나보다 점수가 높은 사람을 전부 세야 하므로 하위권 사용자 한 명의 조회가 99만 행을 훑습니다. 그리고 그 쿼리가 사용자마다 매 요청 들어오고, 동시에 점수 갱신 쓰기가 같은 인덱스로 몰립니다. 정렬 집합은 스킵 리스트와 해시 테이블을 함께 유지해 순위 조회와 범위 조회를 모두 O(log N) 으로 만듭니다. 100만 명이든 1000만 명이든 밀리초입니다. 이 실습에서 특히 신경 써야 할 부분은 6번 스텝의 동점 처리입니다. Redis 의 기본 동점 규칙은 멤버 문자열 사전순인데, 제품 요구는 거의 항상 "먼저 달성한 사람이 위"입니다. 점수와 시각을 하나의 실수로 접는 이 기법은 리더보드를 만들 때마다 쓰게 됩니다.

단계

1. /root/lb/load.py/opt/fixtures/rd/plays.csv(헤더 user_id,points,ts)를 읽어 lb:global 에 사용자별 누적 점수를 넣는다. ZCARD lb:global 이 200 이어야 한다.
2. 상위 10명을 점수와 함께 /root/lb/top10.txt<순위> <user_id> <점수> 형식으로 10줄 적는다. 1위가 첫 줄이다.
3. /root/lb/myrank.txt 에 사용자 u042 의 정보를 user=u042 rank=<1부터> score=<점수> 한 줄로 적는다.
4. /root/lb/incr.pyu042 에 50점을 원자적으로 더한다. 소스에 ZSCORE 로 읽어 ZADD 로 쓰는 패턴이 있으면 안 된다. 실행 후 점수가 정확히 50 늘어야 한다.
5. /root/lb/page.txt 에 100위부터 109위까지 10명을 <순위> <user_id> 형식으로 적는다. 첫 줄의 순위가 100 이어야 한다.
6. /root/lb/tie.pylb:tie 를 만든다. 점수는 points + (1 - ts / 1e10) 로 접는다. /opt/fixtures/rd/ties.csv 의 동점자 5명이 ts 오름차순으로 상위에 오게 하고 결과를 /root/lb/tie.txt 에 5줄 적는다.
7. lb:weekly:2026-W34 를 만들고 TTL 을 604800 이하로 건다. ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2 로 합산 키를 만든다. ZCARD lb:combined 가 200 이상이어야 한다.
8. /root/lb/api.py 를 127.0.0.1:8150 에 띄운다. GET /rank/u042{"user":"u042","rank":<정수>,"score":<수>,"top3":[...]} 를 준다. /root/lb/bench.txtqueries=1000 total_ms=<정수> avg_ms=<수> 를 적고 avg_ms 는 5 미만이어야 한다.

참고

단계 8개

  1. 플레이 기록으로 리더보드 채우기
  2. 상위 10명 뽑기
  3. 특정 사용자의 순위 구하기
  4. 점수를 원자적으로 누적하기
  5. 100위대 페이지 뽑기
  6. 동점을 먼저 달성한 순으로 정렬하기
  7. 주간 랭킹과 합산 만들기
  8. 순위 조회 API 띄우고 성능 재기