-
redis의 Sorted Sets을 활용한 리더보드 구현대용량 데이터 & 트래픽/대용량 처리를 위한 redis 2024. 3. 11. 13:31
게임에서의 랭킹과 같은 리더보드는 RDBMS로 구현하면 한 개 데이터의 조회는 빠르지만 정렬된 데이터와 범위 탐색은 느리다.
redis의 Sorted Sets은 정렬되어 있는 set이기 때문에 업데이트와 범위 탐색이 효율적이다.
RankingService
@Service public class RankingService { private static final String LEADERBOARD_KEY = "leaderBoard"; @Autowired StringRedisTemplate redisTemplate; public boolean setUserScore(String userId, int score) { ZSetOperations zSetOps = redisTemplate.opsForZSet(); zSetOps.add(LEADERBOARD_KEY, userId, score); return true; } public Long getUserRanking(String userId) { ZSetOperations zSetOps = redisTemplate.opsForZSet(); Long rank = zSetOps.reverseRank(LEADERBOARD_KEY, userId); return rank; } public List<String> getTopRank(int limit) { ZSetOperations zSetOps = redisTemplate.opsForZSet(); Set<String> rangeSet = zSetOps.reverseRange(LEADERBOARD_KEY, 0, limit - 1); return new ArrayList<>(rangeSet); } }
redisTemplate을 선언한 뒤 메서드를 차례대로 보면 setUserScore는 userId와 score를 입력받아서 zSetOperations에 add를 하는 것이다. Sorted Set이기 때문에 이미 있는 회원의 데이터를 한 번 더 입력하면 자동으로 덮어씌어지는 방식으로 동작한다. getUserRanking은 userId를 입력받으면 set 안에서 userId의 순위를 반환해주는 함수이고, getTopRank는 범위를 출력해주는 함수이다.
점수가 높은 사람이 0, 1, 2 순으로 나오게 하는 내림차순을 위해 reverseRank와 reverseRange를 사용했다.
위의 로직들은 점수 등록, 조회 등이 잘 작동한다.
redis를 이용하는 이유가 성능이 좋아서라고 했는데 실제로도 그런지 테스트 해보자.
정렬 알고리즘을 이용한 테스트
@Test void inMemorySortPerformance() { ArrayList<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000000; i++) { int score = (int)(Math.random() * 1000000); // 0 ~ 999999 list.add(score); } Instant before = Instant.now(); Collections.sort(list); // nlogn의 시간 복잡도를 가짐 Duration elapsed = Duration.between(before, Instant.now()); System.out.println(elapsed.getNano() / 1000000 + " ms"); }
100만개의 데이터를 생성하고 정렬할 때의 시간을 측정해 보았다.
위와 같이 661ms가 걸렸다.
Redis를 이용한 테스트
@Test void insertScore() { for (int i = 0; i < 1000000; i++) { int score = (int)(Math.random() * 1000000); // 0 ~ 999999 String userId = "user_" + i; rankingService.setUserScore(userId, score); } }
redis에 데이터를 입력할 때에는 정렬을 해야하기 때문에 오랜 시간이 걸린다.
하지만 정렬된 데이터의 조회 시 이점이 있는 것이고, 입력을 할 때에는 100만개의 데이터를 캐시에 저장할 일이 거의 없으니 조회의 성능을 비교하는 것이 목적이니 넘어가도록 하겠다.
@Test void getRanks() { rankingService.getTopRank(1); // 네트워크 첫 연결시 걸리는 시간이 오래걸리므로 의미 없는 호출 넣기 // 1) Instant before = Instant.now(); Long userRank = rankingService.getUserRanking("user_100"); Duration elapsed = Duration.between(before, Instant.now()); System.out.println(String.format("Rank(%d) - Took %d ms", userRank, elapsed.getNano() / 1000000)); // 2) before = Instant.now(); List<String> topRank = rankingService.getTopRank(10); elapsed = Duration.between(before, Instant.now()); System.out.println(String.format("Range - Took %d ms", elapsed.getNano() / 1000000)); }
실제 순위를 조회하는 테스트이다. redis에 100만개의 캐시가 이미 들어가있는 상황이고, 맨 위의 줄은 네트워크 첫 연결시 걸리는 시간을 제외하기 위해 넣어줬다.
1번은 user_100이라는 user의 등수를 가져오는 호출이며 2번은 상위 10개의 랭킹을 출력하는 호출이다.
100만개의 데이터 중에서 45만등의 랭킹을 출력하는 로직은 5ms, 상위 10개의 범위를 찾는 로직은 3ms가 소요되었다.
매우 짧은 시간이 소요되었으며, 정렬된 데이터가 필요할 때에는 Sorted Set을 이용하는 것이 효율적이라는 것을 알게 되었다.
'대용량 데이터 & 트래픽 > 대용량 처리를 위한 redis' 카테고리의 다른 글
Redis의 Pub/Sub을 이용한 채팅방 기능 구현 (0) 2024.03.11 서비스 속도를 높이는 redis의 캐시레이어 (0) 2024.03.10 분산 환경에서의 session 스토어 (0) 2024.03.10 Redis의 data type (0) 2024.03.10 In-memory DB인 Redis의 특징 (0) 2024.03.10