LabHub
배우기 러닝패스 코스

Redis and Caching

Six Data Structures and Where Each Belongs

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

Redis 를 잘 쓴다는 것은 명령을 많이 아는 것이 아니라, 문제에 맞는 자료구조를 고르는 것이다.

Concept map: Sorted Set 이 의외로 넓게 쓰입니다. · 그 메시지는 없어집니다. · XPENDING 이 핵심입니다. · 하나의 키가 전체의 큰 몫을 차지하고 있지 않은지

왜 이게 필요했나

많은 팀이 Redis 를 String 하나로만 씁니다. 전부 JSON 으로 직렬화해 SET 하고 GET 합니다. 동작은 합니다. 그런데 사용자 프로필의 이메일 한 필드만 바꾸려 해도 전체를 읽어 파싱하고 수정해 다시 씁니다. 동시에 두 요청이 그렇게 하면 한쪽이 덮어씁니다.

Hash 를 쓰면 HSET user:42 email a@b.com 한 줄이고, 다른 필드와 충돌하지 않습니다. 자료구조 선택 하나가 경쟁 조건을 없앱니다.

어떻게 동작하나

String 은 단순 값과 카운터입니다. INCR 이 원자적이라는 점이 중요합니다 — 조회수, 레이트 리밋 카운터, 시퀀스 번호에 씁니다. SET key val NX EX 60 은 분산 잠금의 기본 형태입니다.

Hash 는 필드가 여러 개인 객체입니다. 필드 단위 갱신과 조회가 되고, 필드 수가 적으면 Redis 가 내부적으로 압축된 인코딩을 써서 메모리도 아낍니다.

List 는 순서 있는 목록이고 양쪽 끝 삽입·삭제가 O(1) 입니다. 작업 큐와 최근 활동 목록에 씁니다. 가운데 접근은 O(N) 이라 큰 리스트를 인덱스로 뒤지면 안 됩니다.

Set 은 중복 없는 집합이고 교집합·합집합·차집합이 명령 하나입니다. 태그, 팔로워, 중복 제거에 씁니다. SADD 의 반환값으로 "처음 보는 것인가"를 알 수 있어 멱등성 구현에도 쓰입니다.

ZSet(정렬 집합)은 점수를 가진 집합입니다. 점수로 정렬된 상태가 항상 유지되고, 순위 조회가 O(log N) 입니다. 랭킹, 우선순위 큐, 시계열 윈도우(점수를 타임스탬프로)에 씁니다. 이 코스에서 별도 실습 하나를 통째로 쓸 만큼 중요합니다.

Stream 은 추가 전용 로그입니다. 컨슈머 그룹과 확인응답이 있어 진짜 큐에 가장 가깝습니다. 앞 코스에서 다뤘습니다.

현장에서 만나는 모습

운영에서 가장 조심할 명령은 KEYS 입니다. 전체 키스페이스를 훑으며 그동안 Redis 는 다른 일을 못 합니다. Redis 는 단일 스레드로 명령을 처리하기 때문에, 키가 수백만 개인 인스턴스에서 KEYS * 한 번이 수 초의 전면 정지를 만듭니다. 반드시 SCAN 을 커서로 돌려야 합니다.

같은 이유로 FLUSHALL, 큰 컬렉션에 대한 SMEMBERS / LRANGE 0 -1, DEL 로 거대한 키를 지우는 것(대신 UNLINK)도 조심해야 합니다. "Redis 가 갑자기 느려졌다"의 상당수는 O(N) 명령 하나입니다.

메모리 관점도 알아 둘 만합니다. MEMORY USAGE <key> 로 키별 실제 사용량을 볼 수 있고, 같은 데이터라도 자료구조에 따라 몇 배씩 차이가 납니다.

무엇을 고를지 정하는 표

자료구조 쓰는 자리 대표 명령 주의
String 캐시 값, 카운터 SET, INCR 512MB 상한
Hash 객체의 필드별 갱신 HSET, HGETALL 필드가 많으면 HGETALL 이 무겁다
List 큐, 최근 N개 LPUSH, BRPOP 중간 삽입·조회가 O(n)
Set 중복 제거, 태그 SADD, SINTER 큰 집합의 교집합은 비싸다
Sorted Set 랭킹, 시간순 색인 ZADD, ZRANGEBYSCORE 가장 쓸모가 많다
Stream 이벤트 로그, 소비자 그룹 XADD, XREADGROUP List 보다 큐에 적합

Sorted Set 이 의외로 넓게 쓰입니다. 점수를 타임스탬프로 두면 시간 범위 조회가 되고(ZRANGEBYSCORE), 오래된 것을 지우기도 쉽습니다(ZREMRANGEBYSCORE). "최근 24시간 안의 이벤트" 같은 것을 List 로 만들려 하면 곧 막힙니다.

큐는 List 보다 Stream

List 로 큐를 만들면 BRPOP 으로 꺼내는 순간 메시지가 사라집니다. 처리 중에 소비자가 죽으면 그 메시지는 없어집니다.

Stream 은 소비자 그룹과 확인(ack) 개념이 있습니다.

XADD  orders * type payment amount 52000      # 발행
XREADGROUP GROUP workers w1 COUNT 10 STREAMS orders >   # 읽기(pending 으로 표시)
XACK  orders workers <id>                      # 처리 완료
XPENDING orders workers                        # 아직 확인 안 된 것
XCLAIM orders workers w2 60000 <id>            # 죽은 소비자의 것을 가져오기

XPENDING 이 핵심입니다. 소비자가 죽으면 그 메시지가 pending 에 남고, 다른 소비자가 XCLAIM 으로 가져갑니다. List 로는 이것을 직접 만들어야 합니다.

다만 Stream 도 무한히 자라므로 XADD ... MAXLEN ~ 100000 처럼 상한을 둡니다. ~ 를 붙이면 근사 잘라내기라 훨씬 쌉니다.

대형 키가 만드는 문제

Redis 는 단일 스레드로 명령을 처리합니다. 하나가 오래 걸리면 그동안 전부 멈춥니다.

KEYS *                      → O(n). 절대 쓰지 않는다
HGETALL (필드 10만 개)      → 응답이 크고 오래 걸린다
SMEMBERS (원소 100만 개)    → 같은 문제
DEL (큰 컬렉션)             → 삭제도 O(n) 이다. UNLINK 를 쓴다

UNLINK 는 삭제를 백그라운드로 넘겨 블로킹을 피합니다. 큰 키를 지울 때는 이것을 씁니다.

대형 키를 찾는 것은 redis-cli --bigkeys--memkeys 입니다. 정기적으로 돌려 하나의 키가 전체의 큰 몫을 차지하고 있지 않은지 확인합니다.

다음 실습에서 할 것

여섯 자료구조를 하나씩 직접 다루고, KEYS 대신 SCAN 으로 순회하고, 마지막에 같은 데이터를 다른 자료구조로 저장했을 때의 메모리 차이를 표로 만듭니다.