ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Redis Sorted Set을 이용한 실시간 랭킹 시스템 구축(1)
    프로젝트/WebRTC 화상통화 프로젝트 2022. 8. 1. 02:06
    728x90

    이해하는데 필요한 사전 지식

    - Redis란? 

    - 관계형 DB 지식

    - Docker(Redis를 docker로 띄우기 위해)

    관계형 데이터베이스로 랭킹 구축하기

    예를 들어 영화의 관객수를 가지고 랭킹을 매기기 위한다고 가정하겠습니다.

    https://comart.io/blog/realtime-ranking-with-redis-sorted-set

    이러한 순위는 관계형 데이터베이스로도 간단하게 산출해낼 수 있습니다.

     

    데이터베이스의 테이블 이름은 movie Column에서 영화 이름을 name, 관객 수를 view라고 가정하겠습니다.

    movie 테이블은 name, view 칼럼을 가진다

     

    순위의 경우에는 간단하게 관계형 데이터베이스로도 산출해낼 수 있습니다.

    select * from movie ORDER BY views DESC

    만약 101위에서 200위까지 가져온다면 다음과 같은 쿼리를 작성하면 됩니다.

    select * from movie ORDER BY views DESC LIMIT 101, 100

    또한 특정 영화의 랭킹이 몇 위인지 구하려면 다음과 같은 쿼리를 작성하면 됩니다.

    SELECT COUNT(*) + 1 FROM movie WHERE views > (
    	SELECT views from  movie WHERE name = '어바웃 타임'
    )

    어바웃 타임보다 관객수가 더 많은 영화의 개수를 구한 후 여기서 1을 더하면 랭킹을 구할 수 있습니다.

    "만약 관객수가 동점일 때 어떻게 처리할 것인가?"까지 고려한다면 쿼리는 좀 더 복잡해질 수 있습니다.

     

    대부분의 시스템에서 잘 동작하지만 수백만 또는 그 이상의 Row가 존재한다면 어떻게 될까요?

    파이썬 SQLAlchemy를 사용하여 1000만 개의 랜덤  Movie 데이터를 생성하고 특정 영화의 랭킹을 구하는 쿼리를 날리게 되면 2초 이상의 느린 속도를 보여주게 됩니다.

     

    이런 속도의 API로 랭킹을 보여주다가는 서버가 터질 수도 있습니다.

     

    랭킹 X위부터 Y위까지 쿼리는 최적화가 잘 되지만 특정 X의 랭킹이 몇 위인지 쿼리는 최적화가 어렵습니다.

     

    Redis의 Sorted Set

    레디스에서는 Sorted Set이라는 자료구조를 지원합니다.

    레디스는 Key-Value 저장소입니다.

    기본적으로 Set이기 때문에 Key의 중복을 허용하지 않으며 Value로 정렬된 형태로 저장하기 때문에 특정 Key의 순위가 어떻게 되는지 빠르게 구할 수 있습니다.

     

    시간 복잡도로 O(log n)으로 특정 Key 값의 랭킹 값을 얻을 수 있게 됩니다.

     

    docker로 레디스 실행하기

    docker run -it --rm -p 6379:6379 redis:latest

     

    ZADD

    redis-cli를 통해 Redis에 접속한 후에 ZADD 명령어를 통하여 데이터를 추가할 수 있습니다.

    ZADD (Sorted Set의 이름) (점수), (key)를 입력합니다.

     

    예시

    ZADD movie 17615686 "명량"
    ZADD movie 16266338 "극한직업"
    ZADD movie 14414658 "신과함께-죄와벌"
    ZADD movie 14263980 "국제시장"
    ZADD movie 13977602 "어벤져스: 엔드게임"
    ZADD movie 13747792 "겨울왕국 2"
    ZADD movie 13486963 "아바타"
    ZADD movie 13414484 "베테랑"

     

    ZSCORE

    특정 키 값의 점수를 구하기 위해서는 ZSCORE 명령어를 통하여 키값의 점수를 확인할 수 있습니다.

     

    ZRANGE, ZREVRANGE

    ZRANGE는 오름차순을 기준으로, ZREVRANGE는 내림차순을 기준으로 랭킹 정보를 가져옵니다.

     

    만약 1등부터 3등까지의 데이터를 구하고자 한다면 다음과 같이 확인할 수 있습니다.

    ZREVRANGE movie 1 3

     

    ZRANK

    특정 영화의 랭킹이 몇 위인지 ZRANK 명령어를 통해 쉽게 구할 수 있습니다.

    ZRANK movie "아바타"
    
    #결과(integer) 7

     

    Redis로 실시간 랭킹 시스템 구현하기

    1. docker를 이용하여 redis 설치 및 구동

    docker run -d --name redis_study -p 6379:6379 redis

    redis_study라는 이름으로 redis 이미지를 설치하고 실행시킵니다.

    포트번호 잘 확인하셔야 에러 안 납니다

     

    2. docker ps 명령어로 컨테이너 id 확인하기

    docker ps

     

    3. 컨테이너에 접속하여 redis-cli 실행

    docker exec -it {container id} /bin/bash
    
    docker exec -it 084b0169a835 /bin/bash
    
    redis-cli

    /bin/bash로 실행
    redis-cli 접속

    redis-cli에 접속하여 keys * 명령어를 치면 empty array가 나오게 됩니다.

     

     

    4. 스프링 부트 프로젝트에 Redis 라이브러리 추가하기

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    }

     

    5. RedisConfig 클래스 추가

    package zipzong.zipzong.config.redis;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @Configuration
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port}")
        private int port;
    
        @Bean
        public RedisConnectionFactory redisConnectionFactory() {
            return new LettuceConnectionFactory(host, port);
        }
    
        @Bean
        public RedisTemplate<String, Integer> redisTemplate() {
            RedisTemplate<String, Integer> redisTemplate = new RedisTemplate<>();
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            redisTemplate.setConnectionFactory(redisConnectionFactory());
            return redisTemplate;
        }
    
        @Bean
        public StringRedisTemplate stringRedisTemplate() {
            final StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
            stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
            stringRedisTemplate.setValueSerializer(new StringRedisSerializer());
            stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
            return stringRedisTemplate;
        }
    }

     

    설정을 하지 않으면 Spring이 설정 기본값을 제공하여 127.0.0.1 6379 포트로 접속됩니다.

    spring:
      redis:
        host: localhost
        port: 6379

     

    저 같은 경우에는 자꾸 다음과 같은 에러가 발생해 값을 직접 입력해 주었습니다.

    Could not resolve placeholder 'spring.redis.host' in value "${spring.redis.host}"

    private String host = "localhost";
    
    private int port = 6379;

     

    LettuceConnectionFactory는 redis와 connection을 생성해주는 객체입니다.

     

    RedisTemplate

    redis 서버와 통신을 처리하고 추상화를 통해 사용자가 redis 모듈을 사용하기 쉽도록 다양한 기능을 제공합니다.

    redis는 key, value를 바이트 배열로 저장하는데 StringRedisTemplate를 사용하면 이를 문자열로 저장할 수 있습니다.

     

    6. Redis에 데이터 넣기 테스트

    @SpringBootTest
    @ActiveProfiles("test")
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.AUTO_CONFIGURED) // 실제 DB 사용하고 싶을때 NONE 사용
    public class RedisBasicTest {
        @Autowired
        RedisTemplate<String, String> redisTemplate;
    
        @Test
        void redisConnectionTest() {
            final String key = "a";
            final String data = "1";
    
            final ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
            valueOperations.set(key, data);
    
            final String s = valueOperations.get(key);
            Assertions.assertThat(s).isEqualTo(data);
        }
     }

    테스트가 정상적으로 수행되면 redis에 데이터가 들어가 있습니다.

     

    SortedSet 사용하기

    우리는 여기서 SortedSet을 사용하고 싶습니다.

    공식문서에서 redisTemplate 사용법을 찾아보니 ZSetOpertions를 사용하면 될 것 같습니다.

    일반적으로 redis는 key/value 타입의 저장소입니다.

    하지만 ZSET은 score라는 값을 추가로 등록할 수 있습니다.

    key를 통해 랭킹을 식별할 수 있는 이름을 선언할 수 있습니다.

    value를 통해 랭킹 내에서 구분이 필요한 고윳값(사용자의 아이디)으로 사용할 수 있습니다.

    score를 통해 점수를 순위로 나타낼 수 있습니다.

    package zipzong.zipzong.config.redis;
    
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.data.redis.core.ZSetOperations;
    import org.springframework.test.context.ActiveProfiles;
    
    import java.util.Set;
    
    @SpringBootTest
    @ActiveProfiles("test")
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.AUTO_CONFIGURED) // 실제 DB 사용하고 싶을때 NONE 사용
    public class RedisTest {
        @Autowired
        RedisTemplate<String, String> redisTemplate;
    
        @Test
        @DisplayName("Redis 튜토리얼 해보기")
        void redisConnectionTest() {
    
            //given
            final String key = "a";
            final String data = "1";
    
            final ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
            final ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
    
            String rankingBoard = "test";
    
            valueOperations.set(key, data);
            zSetOperations.add(rankingBoard,"user1",10);
            zSetOperations.add(rankingBoard,"user2",20);
            zSetOperations.add(rankingBoard,"user3",1);
            //value가 곂치면 어떻게 될까? user3의 scores는 1->2로 업데이트 된다.
            zSetOperations.add(rankingBoard,"user3",2);
    
            //user1의 score를 찾는 방법
            Double score = zSetOperations.score(rankingBoard, "user1");
            System.out.println(score);
    
            //삭제하는법
            //zSetOperations.remove(rankingBoard, user3);
            
            //when
            final String s = valueOperations.get(key);
            // rankingBoard = "test" 에 등록된 user2 의 랭킹을 조회한다(올림차순). Redis 명령 중 ZREVRANK 에 해당한다.
            Long ranking = zSetOperations.reverseRank(rankingBoard, "user2");
    
            //then
            Assertions.assertThat(s).isEqualTo(data);
            //랭킹은 0번부터 시작하고 user2는 점수가 가장높으니 0이다.
            Assertions.assertThat(ranking).isEqualTo(0);
    
            //현재 저장된 모든 랭킹 정보들을 추출할 수 있음
            //[DefaultTypedTuple [score=20.0, value=user2], DefaultTypedTuple [score=10.0, value=user1], DefaultTypedTuple [score=2.0, value=user3]]
            Set<ZSetOperations.TypedTuple<String>> rankSet = zSetOperations.reverseRangeWithScores(rankingBoard, 0, -1);
            System.out.println(rankSet);
    
        }
    }

    위의 튜토리얼을 통해서 요구사항의 모든 것을 충족시킬 수 있습니다.

    1. 특정 User의 ranking을 조회할 수 있다.

    2. 특정 User의 Score를 조회할 수 있고 Update 할 수 있다.

    3. 현재 모든 랭킹 정보들을 추출할 수 있다.

     

     

    결론

    Redis를 사용하면 관계형 DB보다 준수한 속도를 보여주고 사용법도 간단합니다.

     

     

    출처

    https://comart.io/blog/realtime-ranking-with-redis-sorted-set

     

    Redis Sorted Set 을 이용한실시간 랭킹 시스템 구축 – Allen Dev Blog

    우리는 일반적으로 Redis 를 캐싱 서버 용도로 사용하는데, 캐싱 서버 외에도 실시간 랭킹 시스템을 구축하는데도 유용하게 사용할 수 있습니다. 이번 포스트에서는 Redis Sorted Set 를 사용하여 실

    comart.io

    https://velog.io/@kenux/Redis-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-Redis-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0

     

    [Spring Boot + Redis] 스프링 부트 Redis 사용해보기

    스프링부트 프로젝트에 Redis 적용해보는 기초적인 내용입니다.

    velog.io

    https://docs.spring.io/spring-data/redis/docs/current/reference/html/#get-started:first-steps:spring

     

    Spring Data Redis

    Some commands (such as SINTER and SUNION) can only be processed on the server side when all involved keys map to the same slot. Otherwise, computation has to be done on client side. Therefore, it is useful to pin keyspaces to a single slot, which lets make

    docs.spring.io

    http://blog.zepinos.com/java-redis/2017/09/09/Redis-ZSET-01.html

     

    zepinos BLOG

    zepinos SW Developer Blog.

    blog.zepinos.com

    https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/core/ZSetOperations.html

     

    ZSetOperations (Spring Data Redis 2.7.2 API)

    Set reverseRangeByScore(K key, double min, double max, long offset, long count) Get elements in range from start to end where score is between min and max from sorted set ordered high -> low.

    docs.spring.io

     

     

    댓글

Designed by Tistory.