Redis sorted set?

이전 프로젝트에서 게이트웨이 대기열을 구현할 때 Redis의 Sorted Set을 이용하였다. API 요청이 들어왔을 때 현재 가용 인원 수를 파악하고 알맞는 queue에 적재하는 방식으로 처리하였다.

이후 (사진에는 30초지만) 3초에 한번씩 비활성 유저를 확인한 뒤 active queue에서 제거하고 대기 큐에서 옮겨주는 방식으로 설계하였다.

Sorted Set 특징

  • Redis의 데이터 구조
  • 중복되지 않는 값(Unique value)과 해당 값에 할당된 점수(score)를 함께 저장
  • 점수를 기준으로 오름차순 정렬된 컬렉션
  • 동일한 socre를 갖는 경우 사전순으로 정렬

여기서 값이 중복되지 않는다는 점과 점수를 기준으로 정렬한다는 점에서 sorted set을 선택했다.

여기서 unique value는 각 유저의 id, score는 유저가 요청을 보낸 시간으로 지정하여 저장하였다. 만약 대기큐에 있는 상태에서 요청을 또 보내게 된다면 순서가 밀리게 된다..~~ 마찬가지로 활성 큐에 있는 유저가 일정시간동안 요청이 없어 점수가 갱신되지 않으면 제거 대상이 되어 다시 요청하면 대기해야한다.

대기열 구현 방식에도 두 가지 방식이 있었다

  1. 은행 창구 방식
  2. 놀이공원 방식

은행창구 방식은 말 그대로 은행 창구처럼 대기열에 등록된 순서대로 서비스를 제공하는 방식이다. 사용자들이 대기할 때 현재 대기 순위를 알 수 있다.

놀이공원 방식은 일정 시간 이후에 일정 사용자에게 서비스를 제공하는 방식이다. 사용자에게 예측 가능한 대기시간을 제공한다.

나는 은행창구 방식을 선택하였는데 처리 가능한 서비스가 있으면 즉시 요청을 할당할 수 있고, 놀이공원 방식을 사용하면 결국 사용자가 점점 늘어나 서비스에 부하를 줄 수도 있을 것 같다는 생각이 들었다.

  • priority queues 우선순위 대기열
  • low-latency leadrboards 저지연 리더보드
  • secondary indexing 보조 인덱싱
  • 대규모 온라인 게임에서 가장 높은 점수의 정렬된 목록 유지 (리더보드)
  • 속도 제한기 - 정렬된 set을 사용하여 슬라이딩 윈도우 속도 제한기 빌드 과도한 API 요청 방지
public Mono<RegisterUserResponse> registerUser(String userId) {  
  return reactiveRedisTemplate.opsForZSet().size(USER_QUEUE_ACTIVE_KEY)  
      .flatMap(activeUsers -> {  
        if (activeUsers < MAX_ACTIVE_USERS) {  
          return reactiveRedisTemplate.opsForZSet()  
              .add(USER_QUEUE_ACTIVE_KEY, userId, getCurrentTime())  
              .thenReturn(new RegisterUserResponse(activeUsers + 1));  
        }  
        return reactiveRedisTemplate.opsForZSet()  
            .add(USER_QUEUE_WAIT_KEY, userId, getCurrentTime())  
            .flatMap(success ->  
                reactiveRedisTemplate.opsForZSet().rank(USER_QUEUE_WAIT_KEY, userId)  
            )  
            .map(rank -> new RegisterUserResponse(rank + 1));  
      });

~~서비스 구현 후 처리량을 테스트하였는데… active queue에 적재될 때는 100/sec 정도의 처리량이라면, 대기큐에 들어갈 땐 거의 900/sec 이상의 처리량이 나왔다… 왜 이렇게 차이가 나는지 아직 이유를 모르겠다… ㅠ ~~

Ref