LabHub
배우기 러닝패스 코스

Redisとキャッシュ

リーダーボードを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 미만이어야 한다.

참고

플레이 기록으로 리더보드 채우기

/root/lb/load.py/opt/fixtures/rd/plays.csv(헤더 user_id,points,ts)를 읽어 lb:global 에 사용자별 누적 점수를 넣는다. ZCARD lb:global 이 200 이어야 한다.

/opt/fixtures/rd/plays.csv 를 읽어 사용자별 점수를 정렬 집합에 넣습니다. 같은 사용자가 여러 번 나옵니다 — 누적해야 합니다.

상위 10명 뽑기

상위 10명을 점수와 함께 /root/lb/top10.txt<순위> <user_id> <점수> 형식으로 10줄 적는다. 1위가 첫 줄이다.

높은 점수부터 가져오는 범위 명령이 있습니다. 점수도 함께 받아야 파일에 적을 수 있습니다.

특정 사용자의 순위 구하기

/root/lb/myrank.txt 에 사용자 u042 의 정보를 user=u042 rank=<1부터> score=<점수> 한 줄로 적는다.

순위 명령은 0부터 셉니다. 사람에게 보여 줄 때는 1을 더해야 합니다.

점수를 원자적으로 누적하기

/root/lb/incr.pyu042 에 50점을 원자적으로 더한다. 소스에 ZSCORE 로 읽어 ZADD 로 쓰는 패턴이 있으면 안 된다. 실행 후 점수가 정확히 50 늘어야 한다.

읽어서 더해 쓰면 동시 요청에서 값이 사라집니다. 한 명령으로 더하는 방법이 있습니다.

100위대 페이지 뽑기

/root/lb/page.txt 에 100위부터 109위까지 10명을 <순위> <user_id> 형식으로 적는다. 첫 줄의 순위가 100 이어야 한다.

범위 명령의 시작과 끝 인덱스를 계산하세요. 100위는 인덱스 몇 번인지 주의하세요.

동점을 먼저 달성한 순으로 정렬하기

/root/lb/tie.pylb:tie 를 만든다. 점수는 points + (1 - ts / 1e10) 로 접는다. /opt/fixtures/rd/ties.csv 의 동점자 5명이 ts 오름차순으로 상위에 오게 하고 결과를 /root/lb/tie.txt 에 5줄 적는다.

점수와 시각을 하나의 실수로 접습니다. 소수부가 0과 1 사이여야 정수 경계를 넘지 않습니다.

주간 랭킹과 합산 만들기

lb:weekly:2026-W34 를 만들고 TTL 을 604800 이하로 건다. ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2 로 합산 키를 만든다. ZCARD lb:combined 가 200 이상이어야 한다.

기간을 키 이름에 넣고 만료를 걸면 정리가 자동입니다. 두 집합을 가중치와 함께 합치는 명령이 있습니다.

순위 조회 API 띄우고 성능 재기

/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 미만이어야 한다.

앞에서 만든 것들을 하나의 엔드포인트로 묶습니다. 1,000회 조회 시간을 재면 왜 DB 대신 이걸 쓰는지 숫자로 보입니다.