Redis Sorted Set

나는 과일 장수이다. 그리고 나는 판매하는 과일의 가격을 아래와 같이 손님들에게 보여주고 있다.

이 때, 아래와 같은 조건들을 만족해야 한다.

  1. 각 과일의 가격은 자주 업데이트된다.
  2. 각 과일은 유일(unique)하다.
  3. 가격의 오름차순/내림차순으로 정렬된 과일 목록을 매번 고객에게 보여주어야 한다.

이 모든 조건 안에서 가장 효율적인 자료구조를 이용하여 과일을 데이터로 저장할려고 한다.
단순히 1, 2번을 고려했을 때는 Map, Dictionary와 같은 key/value 형태의 자료구조를 쉽게 떠올릴 수 있다. key를 과일, value를 가격으로 저장하면 1, 2번에 대해 효율적으로 처리할 수 있다.
하지만 3번의 경우를 생각해보면, 매번 저장된 key, value들을 모두 꺼내서 value에 대해 정렬해야하고, 또 정렬된 각 value에 해당하는 key들을 함께 최종적으로 반환해야되므로 시간적, 공간적으로 비용이 많이 든다.
이 때 Redis에서 제공해주는 Sorted Set 자료구조를 이용하면 효율적으로 처리할 수 있다.


Sorted Set이란?

일반적으로 Set 자료구조는 저장된 value들을 unique하게 관리하기 위해 사용된다. 이 때 저장된 value들 사이의 순서는 관리되지 않는다.
하지만 Redis에서 제공해주는 자료구조 중 하나인 Sorted Set(또는 ZSET, 둘다 동일한 말이다)은, 이름 그대로 Set의 특성을 그대로 가지면서 추가적으로 저장된 value들의 순서도 관리해준다. 이 때 이 순서를 위해 각 value에 대해 score를 필요에 맞게 설정할 수 있으며, 이 score를 기반으로 정렬이 된다.


Sorted Set 구조

구조를 보면 우리가 쉽게 접한 단순한 key/value 형태의 자료구조보다 복잡하다. key, member, score 라는 새로운 용어가 등장하기 때문에 더 복잡해 보이기도 하다.
여기서 key 는 Redis의 다른 자료구조들과 마찬가지로 단순히 하나의 ZSET 에 부여되는 key 이름이다.
ZSET은 key/value 형태의 자료구조이고, 여기서 key는 member, value는 score 라고 부른다. 하나의 ZSET에서 member는 unique하고, member 값을 통해 시간복잡도 O(log(N))으로 해당하는 value에 접근할 수 있다. (ZRANK 참고)
score 은 부동 소수점 수만 허용되고, 이 score 값을 기준으로 ZSET 내의 각 원소들이 순서를 가지게 된다.


Sorted Set 명령어

Sorted Set은 다양한 명령어들을 제공해준다. 이 중에서 가장 많이 사용되는 기본 명령어들에 대해서만 한번 살펴보도록 하자. 앞의 과일 장수 예시를 이용하여 각 명령어를 실행해보자.

ZADD

ZADD key score member

먼저 ZSET에 각 과일의 이름과 가격 데이터를 추가해보자. 과일 데이터를 저장할 ZSETkeyfruit 라고 정했다.

127.0.0.1:6379> ZADD fruit 2 apple  
(integer) 1
127.0.0.1:6379> ZADD fruit 10 strawberry  
(integer) 1

복수 개의 데이터를 한번에 넣을 수도 있다.

127.0.0.1:6379> ZADD fruit 8 melon 4 orange 15 pineapple 5 banana  
(integer) 4

이미 존재하는 member 값이라면 score 값을 변경한다. 만약 apple의 가격이 20$으로 올랐다면, 아래와 같이 변경하면 된다.

127.0.0.1:6379> ZADD fruit 20 apple  
(integer) 0


ZSCORE

ZSCORE key member

banana의 가격은 아래와 같이 조회하면 된다.

127.0.0.1:6379> ZSCORE fruit banana  
"5"


ZRANK

ZRANK key member

melon이 몇 번째로 싼 과일인지는 아래와 같이 조회하면 된다.

127.0.0.1:6379> ZRANK fruit melon  
(integer) 2


ZRANGE

ZRANGE key start stop

ZRANGE에서 start, stop 에는 정렬된 원소들 중에서 내가 출력하고 싶은 원소의 시작 위치와 끝 위치를 넣으면 된다. 한가지 유념해야 할점은 첫번째 원소를 0이라 했을 때의 상대적인 위치값이고, 양수/음수 모두 가능하다.

ZRANGE를 이용하여 전체 과일을 가격이 낮은 순서대로 모두 출력하고자 한다.

127.0.0.1:6379> ZRANGE fruit 0 -1  
1) "orange"  
2) "banana"  
3) "melon"  
4) "strawberry"  
5) "pineapple"  
6) "apple"  

가격이 가장 낮은 과일, 가장 높은 과일만을 출력하고 싶다면,

127.0.0.1:6379> ZRANGE fruit 0 0  
1) "orange"  
127.0.0.1:6379> ZRANGE fruit -1 -1  
1) "apple"  

가격이 낮은 순서대로 나열했을 때, 2번째부터 3번째까지의 과일을 출력하고 싶다면,

127.0.0.1:6379> ZRANGE fruit 2 3  
1) "melon"  
2) "strawberry"  

만약 과일의 가격도 함께 출력하고 싶다면, WITHSCORES 옵션을 추가하면 된다.

127.0.0.1:6379> ZRANGE fruit 0 -1 WITHSCORES  
 1) "orange"
 2) "4"
 3) "banana"
 4) "5"
 5) "melon"
 6) "8"
 7) "strawberry"
 8) "10"
 9) "pineapple"
10) "15"  
11) "apple"  
12) "20"  

가격이 높은 순서대로 출력하고 싶다면, ZREVRANGE 명령어를 이용하자.

127.0.0.1:6379> ZREVRANGE fruit 0 -1 WITHSCORES  
 1) "apple"
 2) "20"
 3) "pineapple"
 4) "15"
 5) "strawberry"
 6) "10"
 7) "melon"
 8) "8"
 9) "banana"
10) "5"  
11) "orange"  
12) "4"  


ZRANGEBYSCORE

ZRANGEBYSCORE key min max

6$ 이상 15$ 이하 가격의 과일들을 가격이 낮은 순서대로 출력하고자 한다.

127.0.0.1:6379> ZRANGEBYSCORE fruit 6 15 WITHSCORES  
1) "melon"  
2) "8"  
3) "strawberry"  
4) "10"  
5) "pineapple"  
6) "15"  

5$ 초과 15$ 미만 가격의 가격의 과일들을 가격이 낮은 순서대로 출력하고자 한다면,

127.0.0.1:6379> ZRANGEBYSCORE fruit (5 (15 WITHSCORES  
1) "melon"  
2) "8"  
3) "strawberry"  
4) "10"  

가격 상관없이 모든 과일들을 가격이 낮은 순서대로 출력하고자 한다면,

127.0.0.1:6379> ZRANGEBYSCORE fruit -inf +inf  
1) "orange"  
2) "banana"  
3) "melon"  
4) "strawberry"  
5) "pineapple"  
6) "apple"  

10$ 보다 비싼 모든 과일들을 가격이 높은 순서대로 출력하고자 한다면,
(ZREVRANGEBYSCORE에서는 minmax 의 위치가 ZRANGEBYSCORE에서와 반대다.)

127.0.0.1:6379> ZREVRANGEBYSCORE fruit +inf (10 WITHSCORES  
1) "apple"  
2) "20"  
3) "pineapple"  
4) "15"  


ZREM

ZREM key member

pineapple을 더이상 판매하지 않기로 하여 pineapple 데이터를 지울려고 한다.

127.0.0.1:6379> ZREM fruit pineapple  
(integer) 1
127.0.0.1:6379> ZRANGE fruit 0 -1  
1) "orange"  
2) "banana"  
3) "melon"  
4) "strawberry"  
5) "apple"  

ZADD 와 마찬가지로 한번에 복수개의 삭제도 가능하다.

127.0.0.1:6379> ZREM fruit strawberry orange  
(integer) 2
127.0.0.1:6379> ZRANGE fruit 0 -1  
1) "banana"  
2) "melon"  
3) "apple"  

참고