Redis 자료구조 실습은 cli를 통해서 진행한다. 자료구조 실습을 위한 설치는 다음 문서를 참고한다.
실습에서 다루는 commands는 해당 자료구조의 모든 commands를 포함하지않는다. 대표 commands를 소개한다. 모든 commands의 리스트와 각각의 특징은 링크에서 확인할 수 있다.
Redis 의 모든 자료구조는 Key-value 형식이다. 저장과 조회는 key를 기준으로 한다.
Key 는 binary sequence로 binary-safe하다.
즉, string(empty string포함) 이나 어떤 파일을 binary로 변환한 값이나 상관이 없다.
앞에서부터 byte단위로 비교한다.
Key 설계와 관련해서 다음과 같은 것을 고려해야한다.
Key의 길이(크기)가 너무 길면 다음과 같은 문제가 발생할 수 있다.
Redis는 최대 512MB까지 key를 허용하지만, 일반적으로 1KB(1024 bytes)를 넘기지 않도록 관리하는 것이 좋다.
Key에 사용되는 데이터가 크다면, hashing을 통해서 key의 unique함을 보장하면서도 특정 길이로 줄이는 방법을 추천한다.
SHA1 어떤 input을 넣던지 같은 input에 대해서 해싱 결과가 같은 랜덤한 바이트의 연속이 나오고
160비트, 20바이트를 써서 표현 할 수 있다.
Avoid long key 가이드를 따르기 위해서 일부 사용자는 너무 짧은 키를 고안한다.
이는 메모리를 조금이라도 더 적게 사용하기 때문에 좋은 선택같아 보인다. 하지만 크게 차이가 없고, 비교연산 등에서도 큰 차이가 없다. 하지만 줄인 key 의 의미를 파악할 수 없는 경우 개발, 운영 과정에서 버그나 문제를 야기할 수 있다. 이것은 성능보다 더 큰 문제를 야기할 수 있다.
또한 의미를 알아보기 힘든 축약어로 한 자료구조의 키를 설계하면, Redis 클러스터를 이용하는 경우 key의 분산을 위해서 다른 키도 이와같이 설계해야 한다. 그렇지 않으면 데이터의 분산이 골고루 이루어 지지 않고, 클라이언트의 요청이 한 노드에 몰려서 부하가 분산이 되지 않기 때문이다.
여러 종류의 키를 이런식으로 설계하면 결국에는 어떤 키가 어떤 의미인지 따로 문서 등이 없다면 알아보기 힘들게 되고, 이것은 심각한 버그나 문제를 야기할 수 있다.
따라서 Key가 사이즈가 크지도 않은데 일부러 축약어 등으로 과도하게 줄이는 것은 바람직하지 않다.
의미 파악이 어렵고 가독성이 낮아 유지보수와 운영 중 실수를 유발할 수 있다.
Redis 클러스터 환경에서 key 분산이 제대로 되지 않을 수 있어 부하가 한쪽에 집중될 위험이 있다.
Key 길이를 줄이는 것이 성능에 큰 영향을 주지 않으므로, 축약보다 명확한 의미 전달이 더 중요하다
키안에 포함되어 있는 정보가 의미가 있다면, schema를 가지는 것은 좋은 방법이다.
<예시>
object-type:id
dash(-), dot(.) 으로 구분한 multi word
이 방법은 키의 내용과 의미가 일치하고, 비교연산에서도 같은 자리를 비교하게 되므로 비용 효율적이다.
코멘트 데이터를 비교할 때는, 너무 짧은 키를 피해야 한다는 점(Avoid too short key)도 중요하지만, 일관된 스키마를 유지하는 것(Try to stick with schema)이 더욱 중요하다.
스키마를 정해두면 특정 키 구조 내에서 같은 위치의 정보를 비교하게 되어 비교 연산이 효율적이다.
앞으로 Redis 키를 설계할 때는 다음과 같은 절차를 따르는 것이 좋다:
1. 스키마를 먼저 정의한다.
2. 각 구성 요소의 길이가 너무 길거나 짧지 않은지 검토한다.
또한 운영 중 데이터가 많지 않은데도 Redis 성능이 불안정하거나 응답 속도가 들쭉날쭉하다면, 키의 길이나 패턴을 점검해보는 것이 좋다. 대부분의 키는 코드에서 자동 생성되므로, 예기치 않게 너무 긴 문자열이 포함되었을 가능성도 고려해야 한다.
Redis의 String 은 bytes를 그대로 저장한다. 즉, text 뿐만 아니라 어떤 object나 binary를 bytes 로 serialized 한 형태로 저장한다.
SET
: 지정한 key에 원하는 value 를 저장한다.
EX
seconds -- 지정한 시간(초, seconds)만큼 지난 뒤에 expirePX
milliseconds -- 지정한 시간(밀리초, milliseconds)만큼 지난 뒤에 expire (P
recision / P
artial)EXAT
timestamp-seconds -- 지정한 시간(unix timestamp seconds)에 expirePXAT
timestamp-milliseconds -- 지정한 시간(unix timestamp milliseconds)에 expireNX
-- SETNX와 같음 (set if not exists)XX
-- Key가 존재하면 Set.KEEPTTL
-- Key가 존재하는 시간. EX, PX 류와 동시사용 불가.GET
-- Set하기 이전의 string을 리턴. 이전에 존재하지 않았으면 nil을 리턴. SET
이전에 존재하던 값(value)이 String이 아니면 Set을 취소하고 error를 리턴SETNX
: 지정한 key가 존재하지 않으면, key와 value를 저장한다.
GET
: 지정한 key에 저장된 value를 가져온다.
MGET
: 여러개의 key에 저장된 값을 한 번에 가져온다.
지정된 key에 대해서 숫자를 atomic하게 증가/감소 시키고 그 결과를 응답한다. 분산환경에서 덧셈 뺄셈 연산을 lock free로 구현할 때 많이 쓰인다.
Strings의 SET
command로 초기 숫자를 세팅하고 시작한다.
INCR
지정한 key의 value를 1씩 증가시킨다.
INCRBY
지정한 key의 value로 정수형 숫자를 가지고, 그 숫자를 atomic하게 숫자를 증가/감소 시킨다.
INCRBYFLOAT
지정한 key의 value로 소숫점 숫자를 가지고, 그 숫자를 atomic하게 숫자를 증가/감소 시킨다.
지정한 Key에 리스트를 저장할 수 있다.
선입선출 구조(FIFO) 또는 후입선출 구조(LIFO) 모두 구현 가능하다.
리스트에 저장 가능한 최대 원소 수는 2^32 - 1
(약 43억 개)이다.
다양한 큐/스택 구조 구현이 가능하다
Stack, Queue 등을 구현하는데 사용한다.
LPUSH
: 새 원소를 Head에 추가한다.RPUSH
: 새 원소를 Tail에 추가한다.LPOP
: Head의 원소를 지우고 리턴한다.RPOP
: Tail의 원소를 지우고 리턴한다.LLEN
: 리스트의 길이를 리턴한다.LMOVE
: atomic하게 한 리스트의 원소들을 다른 리스트로 옮긴다.LTRIM
: 지정한 range 만큼의 원소들을 남기고 나머지를 지운다.BLPOP
: LPOP과 같다.BLMOVE
: LMOVE와 같다.Redis에서 Set 자료구조를 표현할 수 있다.
Set
은 정렬되지 않은 유니크한 문자열 데이터(member)를 저장하는 집합 자료구조로, 하나의 키(key)가 집합 이름이 되고, 그 아래에 중복 없는 문자열 멤버들을 저장한다.
대부분의 관련 명령어(command)는 내부적으로 해시 테이블 기반으로 구현되어 있어 시간복잡도가 O(1) 이다. 하지만, 일부 예외적인 명령어는 그렇지 않으므로 시간 복잡도에 주의가 필요하다.
하나의 Set에는 최대 2^32 - 1
(약 43억 개)의 멤버를 저장할 수 있지만, 실무에서는 보통 10만 개 이하로 관리하는 것이 권장된다.
SADD
: Set 에 새로운 member를 추가한다.SREM
: Set 에서 특정 member를 삭제한다.SMEMBERS
: Set에 저장된 모든 member를 리턴한다.SISMEMBER
: 주어진 String이 Set에 존재하는지 확인한다.SINTER
: 주어진 두 개 이상의 Set에 모두 존재하는 member들을 리턴한다. (교집합)SCARD
: Set의 size(cardinality)를 조회한다.Redis는 단일 스레드 기반의 구조로, 명령어를 순차적으로 처리하며 내부적으로 별도의 락(lock)을 사용하지 않기 때문에 매우 빠른 응답 속도를 제공한다.
하지만SMEMBERS
,SINTER
와 같이 시간 복잡도가 O(N) 이상인 명령어는, 데이터 양이 많아질 경우 Redis 전체 처리 성능에 병목을 유발할 수 있다.예를 들어,
SMEMBERS
가 한 번 실행될 때 1ms가 소요된다면, 이를 5번 연속 실행할 경우 최소 5ms가 걸리게 되고, 이 시간 동안 다른 모든 요청은 블로킹 상태로 대기하게 된다.Redis는 고속 처리를 목적으로 메모리 기반(in-memory)으로 동작하기 때문에, 이런 느린 명령어 사용은 시스템의 비용 효율성과 설계 취지를 훼손할 수 있다.
따라서 프로덕션 환경에서는 가능한 한 O(1)의 시간 복잡도를 갖는 명령어 위주로 구성하는 것이 성능 최적화 및 안정적인 운영을 위한 권장 사항이다.
정렬기능이 있는 Set 자료구조 이다.
unique string 데이터(member)를 score 정보로 정렬된 형태의 집합으로 저장할 때 쓴다.
같은 score 정보를 가진 memeber가 여러개 있다면, 문자열순(lexicographically)으로 정렬한다.
Sorted Sets의 모든 commands: Link
ZADD
: 새로운 member를 score 값과 함께 추가한다. 이미 존재하는 member라면
score를 업데이트 한다.
ZRANGE
: 주어진 Range에 해당하는 member들을 리턴한다.
ZRANK
: 주어진 member의 rank를 리턴한다. Ranking은 0부터 시작하는 오름차순이다. (0 = lowest)
ZREVRANK
0이 가장 높은 Ranking (0=highest)ZCARD
: 주어진 key에 해당하는 memeber의 수(cardinality)를 구한다.
ZREMRANGEBYSCORE
: 주어진 key에서 주어진 min, max 이내의 (inclusive) score를 가지는 member를 삭제하고, 삭제된 memeber의 수를 리턴한다.
성능 관련 참고사항
시간 복잡도는 O(log(N)+M)
이다.
N
: sorted set에 저장된 전체 member의 수M
: 지워질 대상이 되는 member 수위 시간복잡도 때문에 대량의 데이터가 저장되어있는 자료구조일수록 성능문제가 발생할 수 있다. 따라서 적절한 수를 유지하도록 EXPIRE
또는 ZREMRANGEBYSCORE
로 관리할 필요가 있다.
데이터가 많을수록 성능 저하가 발생할 수 있으므로, 10만 개 이상이 되지 않도록 유지하는 것이 실무적인 가이드이다
예시.
우리가 유저한테 보여줘야하는 정보는 유저점수 top 100 랭킹 정보.
유저 수는 10만명. 유저 점수 데이터를 확인해보니 top100 안에 위치한 대부분의 유저는 10만점이 넘고, top 200, top 1000까지 확인해도 다들 만 점은 넘어있더라.
이런 경우엔 만점이하의 점수들은ZREMRANGEBYSCORE
같은 명령어들을 통해 주기적으로 지워주면 자료가 일정 이상 커지지 않고 불필요한 데이터로 인한 검색 및 삭제 비용을 줄이고, 정렬 및 조회 성능을 안정적으로 유지할 수 있다. (필요 시EXPIRE
명령어와 함께 사용하여 시간 기반 데이터 관리도 수행할 수 있다.)
파라미터 | 설명 |
---|---|
service | Redis의 key로 사용되는 서비스 식별자. 예: "login:user123" |
timestamp | 현재 요청이 들어온 시간 (ms 단위, time.time()*1000 같은 값) |
window | 슬라이딩 윈도우의 시간 범위. 예: 최근 1분 → window = 60000 (ms 단위) |
limit | 해당 시간 범위 내 허용할 최대 요청 횟수. 예: 100 요청 |
input: service, timestamp(of record), window(time, milliseconds), limit
redis.call('ZREMRANGEBYSCORE', service, 0, timestamp - window)
if redis.call('ZCARD', service) < limit
then
redis.call('ZADD', service, timestamp, timestamp(or id of record))
return 'pass'
else
return 'exceeded limit'
end
redis.call('ZREMRANGEBYSCORE', service, 0, timestamp - window)
if redis.call('ZCARD', service) < limit
redis.call('ZADD', service, timestamp, timestamp(or id))
return 'pass'
else
return 'exceeded limit'
end
상황 예시
window=60000
, limit=100
로직 흐름
오래된 요청 삭제
timestamp - window
보다 오래된 요청을 ZSET에서 제거 → 윈도우 밖 요청 제거
남은 요청 수 세기
→ 지금 윈도우 안에 있는 요청이 몇 개인지 확인
허용 조건 판단
HyperLogLog란 하나의 set에서 cardinality(the number of unique data)를 추정하기 위한 데이터 구조이다. HyperLogLog를 이용하면 작은 데이터 공간을 이용해서 많은 수의 데이터의 cardinality를 추정치로 계산할 수 있다. Redis에서는 HyperLogLog를 지원하는 커맨드들이 있다.
HyperLogLog가 어떻게 작은 사이즈의 데이터구조를 가지고 많은 수의 데이터의 unique
count(cardinality)를 계산할 수 있는 지에 대한 수학적인 설명은 다음 글을 참고
Redis의 HyperLogLog는 최대 12 KB 의 공간으로 0.81%
의 표준 오차율로 cardinality를 계산할 수 있다.
하나의 Key에 대해서는 O(1)를 보장하지만, 여러 개의 key를 한 commands에서 이용하는
경우 주의가 필요하다.
만약 하나의 키에 대해 1s가 걸렸다고 하면 두 개의 키를 준 경우 2s가 아닌 3s 이렇게 2배 보다 더 커질 수 있다는 의미.
PFADD
: 주어진 Key의 HyperLogLog 자료구조에 데이터를 추가한다.PFCOUNT
: 주어진 Key들의 HyperLogLog count의 합을 리턴한다.PFMERGE
: 여러개의 HyperLogLog를 하나로 합친다. 합집합.HyperLogLog의 모든 commands: Link
대량의 데이터에 대해서 실시간 추정치 count를 하기위해서 Redis의 HyperLogLog를 이용하는 경우, 아무리 PFADD
command가 O(1)를 보장하더라도, 같은 key에 대해서 처리할 수 있는 요청량은 한계가 있다. (클러스터를 쓰더라도 결국 하나의 key는 하나의 노드에서 처리한다.)
이 경우 key를 분산해서 HyperLogLog 자료구조를 사용한 다음, count가 필요할 때
PFCOUNT
또는 PFMERGE
를 이용해서 최종 cardinality 를 구하는 것을 추천한다.