-
Redis Sorted Set을 이용한 실시간 랭킹 시스템 구축(1)프로젝트/WebRTC 화상통화 프로젝트 2022. 8. 1. 02:06
이해하는데 필요한 사전 지식
- Redis란?
- 관계형 DB 지식
- Docker(Redis를 docker로 띄우기 위해)
관계형 데이터베이스로 랭킹 구축하기
예를 들어 영화의 관객수를 가지고 랭킹을 매기기 위한다고 가정하겠습니다.
이러한 순위는 관계형 데이터베이스로도 간단하게 산출해낼 수 있습니다.
데이터베이스의 테이블 이름은 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
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
https://docs.spring.io/spring-data/redis/docs/current/reference/html/#get-started:first-steps:spring
http://blog.zepinos.com/java-redis/2017/09/09/Redis-ZSET-01.html
'프로젝트 > WebRTC 화상통화 프로젝트' 카테고리의 다른 글
스프링 시큐리티가 OAuth 로그인을 처리하는 방법 (0) 2022.08.03 Spring JPA에 Index 적용하기 (0) 2022.08.02 Base62 인코딩을 활용해 초대링크 만들기 (0) 2022.07.31 JPA Entity 연관관계 구현하기(양방향 연관관계 List Empty 문제 해결) (0) 2022.07.29 Controller 단위테스트 하기 (0) 2022.07.26