본문 바로가기
Redis

Redis 리더보드 만들기

by 이상한나라의개발자 2023. 12. 11.

리더보드의 동작 API

 

빠른 업데이트 / 빠른 조회가 필요

 

  • 점수 생성 / 업데이트 -> ex : setScore(userId, score)
  • 상위 랭크 조회 (범위 기반 조회) -> ex : getRange(1~10)
  • 특정 대상 순위 조회 (값 기반 조회) -> ex : getRank(userId)

 

RDB를 사용했을 경우의 성능 문제

 

UserId Score
UserA 1000
UserB 900
UserC 950
... ...

 

 

업데이트 측면

 

한 row만 접근하여 update 하므로 빠름

ex : update ranking set score = 990 where userId = 'UserA';

 

랭킹 범위나 특정 대상의 순위 조회

랭킹 범위나 특정 대상의 순위를 조회 할 경우 db에 순위 컬럼이 없으며, 만약 있다고 하더라도 매번 조회할 때마다 집계  함수를 사용하여 집계를 다시 해야한다. 만약 컬럼을 붙여서 조회 하면 된다고 생각할 수도 있지만, 이럴경우 update에 문제가 생긴다. 이유는 수정될때 전체 랭킹을 재정의 해야 하기 때문이다

데이터를 정렬하거나 COUNT() 등의 집계 연산을 수행 하므로 데이터가 많아질수록 속도라 느려짐

ex : select userId from ranking order by score desc limit 0,5

 

 

Redis를 사용했을 때의 장점

 

  • 순의 데이터에 적합한 Sorted-Set의 자료구조를 사용하면 score를 통해 자동으로 정렬됨
  • 용도에 특화된 오퍼레이션(set 삽입 / 업데이트, 조회)이 존재하므로 사용이 간단함.
  • 자료구조의 특성으로 데이터 조회가 빠름 (범위 검색, 특정 값의 순위 검색)
  • 빈번한 액세스에 유리한 In-memory DB 속도

 

리더보드 만들기

 

build.gradle

 

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

 

application.yml

 

spring:
  redis:
    host: localhost
    port: 6379

 

 

Controller

 

@RestController
@RequiredArgsConstructor
public class RankingController {

    private final RankingService rankingService;

    @GetMapping("/setScore")
    public Boolean setScore(@RequestParam String userId, @RequestParam int score) {
        return rankingService.setUserScore(userId, score);
    }

    @GetMapping("/getRank")
    public Long getRank(@RequestParam String userId) {
        return rankingService.getUserRanking(userId);
    }

    @GetMapping("/getTopRank")
    public List<String> getTopRank() {
        return rankingService.getTopRank(3);
    }
}

 

 

Service

 

@Service
@RequiredArgsConstructor
public class RankingService {

    private final StringRedisTemplate redisTemplate;

    private static final String LEADER_BOARD_KEY = "leaderBoard"; // 키는 공통으로 만든다.


    /*
    데이터 저장
     */
    public boolean setUserScore(String userId, int score) {
        // Sorted Set : 정렬된 Set 을 사용하기 위해 ZSetOperations 를 사용한다.
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        zSetOps.add(LEADER_BOARD_KEY, userId, score);

        return true;
    }

    /*
    유저의 랭킹 조회
     */
    public Long getUserRanking(String userId) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        // reverseRank : 높은 점수부터 조회 (내림차순)
        return zSetOps.reverseRank(LEADER_BOARD_KEY, userId);
    }

    /*
    범위 기반 조회
     */
    public List<String> getTopRank(int limit) {
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        // reverseRange : 높은 점수부터 조회 (내림차순)
        Set<String> rangeSet = zSetOps.reverseRange(LEADER_BOARD_KEY, 0, limit - 1);
        assert rangeSet != null;
        return List.copyOf(rangeSet);
    }
}

'Redis' 카테고리의 다른 글

Pub&Sub  (0) 2023.12.11
서비스 속도를 높이는 캐시 레이어  (0) 2023.12.11
분산 환경에서 세션 스토어 만들기  (2) 2023.12.11
Redis 설치 및 문법  (1) 2023.12.11
RDBMS & NoSQL & Redis  (0) 2023.12.11