Skip to content

Redis 로 랭킹시스템 구현

박찬얼 edited this page Sep 13, 2023 · 10 revisions

랭킹 정보 저장에 사용할 데이터 베이스 Mysql vs redis

목적

랭킹 서비스는 1~10위를 메인 페이지에서 보여줄 수 있어야한다.

  • 모든 유저의 점수를 저장하여 점수로 정렬하여 1~10위 유저의 데이터를 응답해줘야한다.
  • 유저가 글을 작성하거나, 유저의 글이 좋아요를 받거나, 유저의 글이 채택 당할때 점수를 증가 시켜줘야한다.
  • 이때 유저의 닉네임, 순위, 프로필 사진을 응답해줘야 한다.

Mysql로 구현

  • 유저가 글을 쓰거나 좋아요를 누르거나 채택 할때 점수를 업데이트 해야하기 때문에 데이터베이스에 접근이 빈번 해진다.
  • 메인 페이지에 접속할 때 마다 모든 유저를 점수로 정렬하여 1~10위 유저 정보를 출력해줘야 하기 때문에 메인 페이지 또한 무거워 질 수 있다고 생각한다.

점수 순위

 SELECT * FROM `user` ORDER BY `score` DESC

101위 부터 200위

SELECT * FROM `user` ORDER BY `score` DESC LIMIT 101, 100

내부조인을 사용한 최적화 방법

SELECT `m`.* FROM `user` `m`
INNER JOIN (
 SELECT id FROM `user`
 ORDER BY `score` DESC LIMIT 101, 100
) `lu` ON (`m`.`id` = `lu`.`id`)

특정 유저의 랭킹 순위 (동점 고려하지 않음) 해당 유저보다 점수가 더 많은 유저의 수를 구한 후 역서 1을 더하면 해당 유저의 랭킹이 나온다.

SELECT COUNT(*) + 1 FROM `user` WHERE `score` > (
 SELECT `score` FROM `user`
 WHERE `name` = `닉네임` 
)

Redis 로 구현시

유저가 글을 쓰거나 좋아요를 누르거나 채택을 할때 유저 데이터에서 점수를 업데이트가 매우 빠르게 가능하기 때문에 해당 동작들이 가볍게 동작한다.

async updateRank(userId: number, score: number) {
        redisClient.zincrby('total_rank', score, String(userId), (err, result) => {
            if (err) console.log(err);
        })

        redisClient.zincrby('month_rank', score, String(userId), (err, result) => {
            if (err) console.log(err);
        })
    }

메인 페이지에서 점수로 유저를 정렬하여 1~10위 유저 정보를 보여주는 것 또한 sorted set을 이용하면 매우 빠르게 정렬된 데이터를 가져올 수 있음

async getTotalRanker(range: number) {
        const getAsync = util.promisify(redisClient.zrevrange).bind(redisClient);
        const temp = await getAsync('total_rank', 0, range - 1, 'withscores');
        return temp;
    }

    async getMonthRanker(range: number) {
        const getAsync = util.promisify(redisClient.zrevrange).bind(redisClient);
        const temp = await getAsync('month_rank', 0, range - 1, 'withscores');
        return temp;
    }

다만 sorted set은 key-value 이기 때문에 유저의 닉네임과 프로필은 redis에 추가로 저장하여 1~10위 유저의 경우 이 정보도 조회하여 가져와야함

그러나 이 동작도 매우 빠르게 수행 가능함

async getTotalRankerInfo(range: number) {
        const totalRanker = await this.getTotalRanker(range);
        const totalRankerInfo = [];
        for (let i = 0; i < range; i++) {
            const id = Number(totalRanker[i * 2]);
            const photo = await this.getUserProfile(id);
            const nickname = await this.getUserName(id);
            const ranker = {
                id: id,
                score: totalRanker[i * 2 + 1],
                photo: photo,
                nickname: nickname
            };
            totalRankerInfo.push(ranker);
        }
        return totalRankerInfo;
    }

Redis 로 랭킹시스템 구현

필요한 데이터

  • 유저 닉네임
  • 유저 프로필 사진
  • 유저 월간 점수
  • 유저 총 점수

유저 월간 점수

  • Sorted set을 사용하여 month_rank 라는 set에 유저 아이디를 key로 유저 월간 점수를 value로 사용하여 저장

유저 총 점수

  • Sorted set을 사용하여 total_rank 라는 set에 유저 아이디를 key로 유저 월간 점수를 value로 사용하여 저장

유저 닉네임

  • Redis set에 userId_nickname 를 key로 닉네임을 value로 사용하여 저장

유저 프로필 사진

  • Redis set에 userId_profile 를 key로 유저 프로필 사진 url 을 value로 사용하여 저장

결과 화면

스크린샷 2021-12-20 오후 9 23 34
Clone this wiki locally