Skip to main content

Command Palette

Search for a command to run...

Redis ZSET

Updated
4 min read
Redis ZSET

ZSET 이란?

ZSET 은 Redis 에서 unique 한 string 들이 score 순서대로(in order) 정렬되어 있는 자료구조이다. 그래서 Leader boardRate limiter 에 쓰일수 있습니다. 기본적으로 자료구조가 Hash Table + Skip List(스킵 리스트) 두가지 자료구조를 합쳐서 사용하기 때문에 접근에는 O(1), 추가에는 O(log N) 이 소요됩니다.

추가(ZADD)

ZADD KEY [NX | XX] [GT | LT] [CH] [INCR] score member
localhost:6379> ZADD roach_set 1 "roach"
(integer) 1
localhost:6379> ZADD roach_set 2 "roach2"
(integer) 1
localhost:6379> ZADD roach_set 3 "roach3"
(integer) 1
localhost:6379> ZADD roach_set 4 "roach4" 5 "roach5"
(integer) 2

위와 같이 명령어를 입력하여 추가할 수 있습니다. [ 친 부분은 Optional 하다고 생각해주시면 됩니다. 실제로 ZSET 에 추가해봅시다.

위와 같이 추가가 잘 되었고 제가 몇개를 넣었는지 리턴해주는 것을 확인할 수 있습니다. 삽입간 정렬이 일어나므로 공식문서에 적힌대로 O(log N) 시간이 소요되는 것을 알 수 있습니다.

범위 검색(ZRANGE)

ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
localhost:6379> ZRANGE roach_set 1 3
1) "roach2"
2) "roach3"
3) "roach4"
localhost:6379> ZRANGE roach_set 1 4
1) "roach2"
2) "roach3"
3) "roach4"
4) "roach5"

ZRANGE 는 score 가 아닌 인덱스 기반으로 조회가 가능한 메소드 입니다. O(log(N)+M) 의 시간복잡도를 가지고 있으며 N 은 sorted set 안의 멤버들의 개수이고, M 은 리턴되는 멤버들의 개수입니다.

첫번째 질의로는 첫번째 인덱스 ~ 세번째 인덱스 까지를 가져오도록 질의했고, 두번째 인덱스로는 첫번째 인덱스 ~ 네번째 인덱스 를 가져오도록 질의하였습니다. 참고로 마지막 인덱스를 -1 로 하면 lastIndex 와 동일한 의미를 지닙니다. 시간복잡도가 꽤 커질수 있으므로 redis 에서도 주의해서 사용하라는 @slow 어노테이션이 붙어있습니다.

스코어 기반 범위 검색(ZRANGEBYSCORE)

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
localhost:6379> ZRANGEBYSCORE roach_set 1 3
1) "roach"
2) "roach2"
3) "roach3"
localhost:6379> ZRANGEBYSCORE roach_set 1 4
1) "roach"
2) "roach2"
3) "roach3"
4) "roach4"

ZRANGEBYSCORE 는 점수기반 범위로 검색하고 시간복잡도는 ZRANGE 와 동일하게 O(log(N)+M) 의 복잡도를 지닙니다.

특별하게 설명할 부분은 없고 점수 기반은 점수 사이에 얼마가 있을지 모르므로 꼭 LIMIT 과 OFFSET 을 잘 활용하여 검색해야 한다는 점만 알아두면 좋을거 같습니다.

삭제(ZREM)

ZREM key member [member ...]
localhost:6379> ZREM roach_set "roach"
(integer) 1
localhost:6379> ZRANGEBYSCORE roach_set 1 4
1) "roach2"
2) "roach3"
3) "roach4"

SET 에서 KEY 와 MEMBER 기반으로 삭제하는 메소드 입니다. 삭제하는 것도 정렬의 오버헤드가 드므로 시간 복잡도는 O(log(N)+M) 이 소요됩니다.

“roach” 라는 key 를 이용해서 해당 Set 에 제거하는 방식입니다.

랭크(ZRANK)

ZRANK key member [WITHSCORE]
localhost:6379> ZRANK roach_set "roach1"
(nil)
localhost:6379> ZRANK roach_set "roach2"
(integer) 0
localhost:6379> ZRANK roach_set "roach2" WITHSCORE
1) (integer) 0
2) "2"
localhost:6379> ZRANK roach_set "roach4" WITHSCORE
1) (integer) 2
2) "4"

ZRANK 는 key 와 member 기반으로 RANK 를 알려주는 메소드입니다. 기본적으로 zero-based(0 부터 시작) 이며 WITHSCORE 와 함께 조회할 시에는 스코어까지 함께 리턴받을 수 있습니다. 시간복잡도는 O(log N) 시간복잡도 안에 수행히 가능합니다.

Rate Limiter 구현

유저가 5초 동안 요청할 수 있는 허용된 요청의 수는 5개 입니다. 요걸 어떻게 ZSET 으로 구현해볼 수 있을까요? 가볍게 생각해보면 1초를(1000ms) 로 잡고 계산하여 이를 score 화 하는 방법이 있습니다. ZSET 자체는 정렬된 자료구조이므로 RANGE 를 이용하여 쉽게 범위 검사가 가능합니다.

localhost:6379> ZADD user:1 1000 req_1
(integer) 0
localhost:6379> ZADD user:1 1100 req_2
(integer) 1
localhost:6379> ZADD user:1 1300 req_3
(integer) 1
localhost:6379> ZADD user:1 2000 req_4
(integer) 1
localhost:6379> ZADD user:1 4000 req_5
(integer) 1
localhost:6379> ZADD user:1 5000 req_6

현재 1초에서 5초사이에 user:1 이 총 6건의 요청을 보낸 것을 확인할 수 있습니다. 이를 확인하기 위해서는 ZCARD 메소드를 이용하면 됩니다. ZCARD 는 현재 SET 의 Cardinality 를 리턴해주므로 중복이 아닌 멤버의 갯수를 리턴해주게 됩니다.

localhost:6379> ZCARD user:1
(integer) 6

ZCARD user:1 을 하게 되면 user:1 에 얼마나 많은 member 가 있는지 확인할 수 있습니다. 5초 동안 허용된 요청수 5를 넘었으므로 user:1 은 더이상 요청을 보내지 못하게 됩니다. 근데 만약 시간이 더 흘러서 5-10초 구간까지 갔다고 해봅시다.

localhost:6379> ZADD user:1 1000 req_1
(integer) 1
localhost:6379> ZADD user:1 1100 req_2
(integer) 1
localhost:6379> ZADD user:1 1300 req_3
(integer) 1
localhost:6379> ZADD user:1 2000 req_4
(integer) 1
localhost:6379> ZADD user:1 4000 req_5
(integer) 1
localhost:6379> ZADD user:1 5000 req_6
(integer) 1
localhost:6379> ZADD user:1 6000 req_7
(integer) 1
localhost:6379> ZADD user:1 10000 req_8
(integer) 1

사실 만약 지금이 10초 부근이라 했을때 5초 이전의 req_1 ~ req_5 요청들은 해당 구간의 sliding window 에 없어야 합니다. 그 경우에는 ZREMRANGEBYSCORE 로 0~5 초 구간의 요청데이터를 지워주면 됩니다.

localhost:6379> ZREMRANGEBYSCORE user:1 0 5000
(integer) 6

ZREMRANGEBYSCORE 는 KEY 기반으로 점수가 min ~ max 구간에 있는 member 들을 제거해줍니다. Return 값을 보면 req_1 ~ req_6 까지 총 6개가 잘 지워진것을 확인할 수 있습니다.

localhost:6379> ZCARD user:1
(integer) 2

이제 ZCARD 를 해보면 총 2 개로 req_7req_8 만 남은것을 확인할 수 있습니다.

마치며

이 글은 복리 효과를 위해 매일 30분정도를 투자하여 작성되는 글입니다 :). 가볍게 읽어주시고 더 정확한 자료는 공식문서를 참고 바랍니다.


References

30 views

More from this blog

RDB 에서 큰 컬럼을 인덱스로 잡으면 안되는 이유

B-Tree 는 기본적으로 페이지 사이즈 와 저장할 수 있는 원소의 개수를 고정값으로 사용한다. 하지만 우리가 실제로 페이지에 저장하는 값은 가변적인 크기를 가지고 있기 때문에 필연적으로 물리적으로 저장해야할 개수가 다 차기도 전에 페이지가 넘치는 상황에 부딪히게 된다. 예를 들어 100KB 를 저장하는 페이지에 위와 같이 데이터를 저장한 상태이다. 여

Feb 26, 20262 min read49

Slotted Page

데이터베이스와 관련된 기술을 보다보면 어떻게 데이터를 관리하고 저장하지? 특히 단편화(Fragmentation) 이 일어나는 것을 어떻게 통제하고 관리할까? 혹은 정렬된 자료구조 내부에서 데이터의 순서를 보존하기 위해 어떠한 행위들을 할까? 궁금해집니다. 오늘은 조금 더 데이터베이스 내부에 쓰이는 자료구조를 들여다보며 연관된 행위를 공부해보려고 합니다. F

Feb 22, 20264 min read63
Slotted Page

MCP 를 통한 workflow 자동화

AI native 최근에 LinkedIn 이나 여러 소셜 플랫폼들의 글을 보면 AI native 회사 라는 워딩들이 많이 보입니다. IBM 의 정의에 따르면 AI native 를 아래와 같이 정의한다고 하는데요. “AI를 사고와 업무 방식에 끊임없이 내재화하는 상태” 그렇다면 팀원들이 계속해서 AI 를 사고와 업무 방식에 끊임 없이 내재화 하려면 어떻게 해야할까요? 개발자들은 이미 Claude code 나 Codex 등 여러 AI Tool...

Feb 14, 20263 min read100

파이썬 톺아보기 2화 - Ast 와 바이트코드

식(Expression) 과 문장(Statement) 프로그래밍을 공부하다보면 위 두 단어를 반드시 마주하게 된다. 가끔 헷갈려하는 경우가 많은데 오늘은 python 에서 기본 모듈인 ast 모듈을 공부하며 이를 알아보도록 하자. 식(Expression) 기본적으로 식(Expression) 이란 평가되면 값이 나오는 코드 조각을 뜻한다. 파이썬에서는 어떠한 부분들이 있을까? 노드 타입설명예시 BinOp이항 연산a + b, x * y...

Feb 6, 20267 min read30
D

dev_roach

41 posts

Redis Sorted Sets Explained