Redis ‐ Redis Keys Management - thought-corner/backend-roadmap GitHub Wiki

Redis - Redis Keys Management

Keys 관리 기법 - KEYS와 SCAN

1. KEYS 명령어(패턴 매칭)

  • 가장 직관적이지만 위험성이 큰 방식이다.
    • 동작 방식 : 데이터베이스에 저장된 모든 키를 훑어서 패턴에 맞는 키를 한 번에 다 가져온다.
    • 특징(전체 키 조회) : 데이터 양이 적을 때는 빠르고 간편하다.
    • 문제점(O(N) 서버 블로킹) : Redis는 싱글 쓰레드 기반으로 동작한다. KEYS 명령어가 수백만 개의 키를 조회하는 동안 다른 모든 클라이언트 요청(읽기/쓰기)은 모두 대기 상태가 된다. 이 때문에 서비스 응답 속도가 급격히 느려지거나 서버가 다운된 것처럼 보이는 장애로 이어질 수 있다.
  • 패턴 문법 : *(0개 이상 문자), ?(한 글자), [abc](문자 집합) — 예) KEYS users:*, KEYS user:?00
  • 운영 환경에서는 rename-command KEYS "" 설정으로 명령어 자체를 봉인해두는 경우도 많다. 로컬·개발 환경에서만 쓰는 것이 원칙이다.

2. SCAN 명령어(커서 기반)

  • KEYS의 단점을 보완한 안전한 대안이다.
    • 동작 방식 : 한 번에 모든 키를 가져오지 않고, 커서(Cursor)를 사용해 데이터를 조금씩 나누어서 가져온다.
    • 특징(일부씩 나눠서 조회) : 클라이언트가 서버에 다음 데이터를 달라고 반복해서 요청하는 방식이다.
    • 장점 : 한 번의 호출당 처리 시간이 매우 짧아 서버를 블로킹하지 않는다. 호출과 호출 사이에 다른 클라이언트의 요청이 정상적으로 처리된다.
SCAN 0 MATCH users:* COUNT 100     # 커서 0에서 시작
# → 1) "3072"  (다음 커서)
#    2) 1) "users:1"  2) "users:2" ...
SCAN 3072 MATCH users:* COUNT 100  # 반환된 커서로 다시 호출
# → 커서가 다시 0이 되면 순회 종료

3. SCAN을 쓸 때 반드시 알아야 할 특성

  • 전체 순회 비용은 결국 O(N)이다. 한 번의 호출이 가벼울 뿐, 전부 훑는 총량은 KEYS와 같다. 차이는 그 작업을 잘게 쪼개 다른 요청을 막지 않는다는 점이다.
  • COUNT는 보장값이 아니라 힌트다.(기본값 10) 반환되는 키 개수는 COUNT보다 많을 수도 적을 수도 있다. "몇 개씩 가져올지"가 아니라 "한 번에 얼마나 훑을지"를 정하는 값이다.
  • MATCH 필터는 서버가 훑은 뒤에 적용된다. 따라서 조건에 맞는 키가 없으면 빈 배열이 반환될 수 있는데, 이것이 순회 종료를 뜻하지 않는다. 종료 조건은 오직 반환된 커서가 0이 되는 것이다.
  • 같은 키가 두 번 이상 반환될 수 있다. 애플리케이션에서 중복 제거(Set에 담는 등)를 해야 한다.
  • 순회 도중 추가·삭제된 키는 반환될 수도, 안 될 수도 있다. 다만 순회 시작부터 끝까지 계속 존재한 키는 최소 한 번은 반드시 반환된다는 것이 SCAN의 보장이다.
  • 타입으로 걸러내려면 SCAN 0 TYPE string(Redis 6.0+)을 쓴다.
  • 형제 명령어 : 컬렉션 내부를 훑을 때도 같은 원리를 쓴다. HSCAN(Hash 필드), SSCAN(Set 요소), ZSCAN(Sorted Set 요소)

Keys 관리 기법 - EXISTS

  • EXISTS key : 사용자가 확인하고 싶은 특정 키 이름을 매개변수로 전달한다.
  • 키 존재 확인 : Redis 내부의 인덱스(Hash Table)에서 해당 키가 등록되었는지 검색한다.
  • 1을 반환하면 해당 키가 존재함을, 0을 반환하면 해당 키가 존재하지 않음을 의미한다.
  • 시간복잡도 O(1) : Redis는 Key-Value를 쌍으로 관리하며 내부적으로 해시 테이블(Hash Table) 구조를 사용하기 때문에 단 한 번의 연산으로 존재 여부를 찾아낼 수 있어 매우 빠르며 서버에 부하를 거의 주지 않는다.
  • 여러 키를 한 번에 확인할 수도 있다. EXISTS k1 k2 k3존재하는 키의 개수를 반환한다.(같은 키를 두 번 쓰면 2로 센다) 이 경우 시간 복잡도는 확인할 키 개수 N에 비례하는 O(N)이다.
  • 값 자체가 필요 없고 존재 여부만 알면 될 때 GET 대신 쓰면 네트워크 전송량을 아낄 수 있다.

Keys 관리 기법 - 키 삭제(DEL / UNLINK)

  • DEL key [key ...] : 키를 삭제하고 실제로 삭제된 개수를 반환한다. 없는 키를 지워도 에러 없이 0으로 센다.(멱등)
  • 시간 복잡도는 값의 종류에 따라 달라진다.
    • String 하나를 지우는 것은 O(1)이지만, 요소가 수백만 개인 List·Set·Hash·Sorted Set을 지우는 것은 O(N) 이다. 내부 요소를 하나씩 해제해야 하기 때문이다.
    • 즉, 거대한 컬렉션에 DEL을 실행하면 그 시간 동안 서버가 블로킹된다. KEYS *와 같은 함정이다.
  • UNLINK key [key ...] (Redis 4.0+)
    • 키를 키 공간에서 즉시 분리하고, 실제 메모리 해제는 백그라운드 스레드가 처리한다.
    • 호출 자체는 O(1)에 가깝게 끝나므로 큰 컬렉션을 지울 때는 DEL 대신 UNLINK를 쓰는 것이 안전하다.
    • lazyfree-lazy-user-del yes 설정을 켜면 DEL도 내부적으로 UNLINK처럼 동작한다.

Keys 관리 기법 - 만료 시간(TTL / EXPIRE / PERSIST)

  • EXPIRE key seconds : 이미 저장된 키에 만료 시간을 부여한다.(밀리초 단위는 PEXPIRE)
  • TTL key : 남은 만료 시간을 초 단위로 확인한다. 반환값의 의미를 구분해서 알아둘 것.
TTL session:100
# 양수 : 남은 시간(초)
# -1   : 키는 있지만 만료 시간이 설정되지 않음(영구)
# -2   : 키 자체가 존재하지 않음
  • PERSIST key : 설정된 만료 시간을 제거해 영구 키로 되돌린다.
  • 키의 값을 덮어쓰면(SET) 기존 TTL이 사라진다. TTL을 유지한 채 값만 바꾸려면 SET key value KEEPTTL을 사용한다. 반면 INCR, HSET, LPUSH처럼 값의 일부만 바꾸는 명령은 TTL을 유지한다.
  • TTL은 키 단위로만 걸린다. Hash의 개별 필드나 List의 개별 요소에는 만료를 걸 수 없으므로, 수명이 다르면 키를 분리해야 한다.

Keys 관리 기법 - 만료된 키는 언제 지워지는가

  • TTL이 지났다고 해서 그 즉시 메모리에서 사라지는 것은 아니다. Redis는 두 가지 방식을 함께 쓴다.
    • Lazy(수동) 만료 : 해당 키에 접근하는 순간 만료 여부를 확인하고, 만료되었다면 그 때 삭제하고 없는 것처럼 응답한다.
    • Active(능동) 만료 : 백그라운드에서 주기적으로 만료 대상 키를 일부 샘플링해 삭제한다. 한 번에 전부 훑으면 블로킹되므로 조금씩 나눠서 처리한다.
  • 그래서 "만료되었지만 아직 메모리를 차지하고 있는 키" 가 존재할 수 있다. TTL이 지난 직후 used_memory가 곧바로 줄지 않는 이유가 이것이다.
  • 다만 조회 결과에는 영향을 주지 않는다. Lazy 만료 덕분에 만료된 키는 어떤 명령으로도 조회되지 않는다.

Keys 관리 기법 - 메모리 한계와 축출 정책(maxmemory-policy)

  • 메모리는 유한하므로 maxmemory로 상한을 정하고, 한계에 도달했을 때의 동작을 maxmemory-policy로 지정한다.
정책 동작
noeviction 아무것도 지우지 않고 쓰기 명령에 에러를 반환한다(기본값). 캐시가 아닌 저장소로 쓸 때 선택
allkeys-lru 전체 키 중 가장 오래 쓰이지 않은 것부터 축출 — 순수 캐시 용도의 일반적인 선택
allkeys-lfu 전체 키 중 사용 빈도가 가장 낮은 것부터 축출(Redis 4.0+). 접근 패턴이 편중될 때 유리
volatile-lru / volatile-lfu / volatile-ttl TTL이 설정된 키 중에서만 축출. 영구 보관 키와 캐시 키가 한 인스턴스에 섞여 있을 때
allkeys-random / volatile-random 무작위 축출
  • 캐시로 쓰면서 noeviction(기본값)을 그대로 두면, 메모리가 차는 순간 쓰기가 전부 실패한다. 캐시 용도라면 반드시 정책을 바꿔야 한다.
  • 반대로 세션·분산 락처럼 사라지면 안 되는 데이터가 함께 있다면 allkeys-*는 위험하다. TTL이 있는 키만 대상으로 하는 volatile-*를 쓰거나 인스턴스를 분리한다.

Keys 관리 기법 - 그 외 유용한 명령어

  • TYPE key : 키에 저장된 값의 자료구조 종류를 반환한다.(string / list / set / hash / zset / stream) 다른 타입에 잘못된 명령을 써서 WRONGTYPE 에러가 날 때 원인 확인용
  • RENAME key newkey : 키 이름을 변경한다. 대상 키가 이미 있으면 덮어쓰므로, 안전하게 하려면 대상이 없을 때만 실행하는 RENAMENX를 쓴다.
  • DBSIZE : 현재 DB의 전체 키 개수를 O(1)로 반환한다. 키 개수만 알고 싶을 때 KEYS *를 세는 대신 이것을 쓴다.
  • RANDOMKEY : 무작위 키 하나를 반환한다. 샘플링·디버깅용
  • FLUSHDB / FLUSHALL : 현재 DB / 전체 DB를 비운다. 운영 환경에서는 사고로 직결되므로 ASYNC 옵션(백그라운드 삭제)과 함께 쓰거나 아예 봉인해둔다.

Keys 관리 기법 - 키 네이밍 컨벤션

  • 콜론(:)으로 계층을 구분하는 것이 사실상의 표준이다.
users:100:profile      # PK가 100인 사용자의 프로필
products:123:details   # PK가 123인 상품의 세부사항
session:abc123         # 세션
lock:order:500         # 분산 락
  • 이렇게 잡아두면 SCAN 0 MATCH users:*처럼 도메인 단위로 순회하거나, 접두사별로 TTL·모니터링 정책을 다르게 가져가기 쉽다.
  • Redis에는 테이블 스키마가 없으므로 키 이름이 곧 스키마 문서 역할을 한다. 팀 전체가 같은 규칙을 쓰는 것이 핵심이다.
  • 키 이름 자체도 메모리를 차지하므로 의미가 통하는 선에서 짧게 유지한다. 다만 지나친 축약은 가독성을 해치므로 균형이 필요하다.
  • 클러스터 모드에서 여러 키를 한 명령으로 다뤄야 한다면 해시 태그(user:{100}:profile, user:{100}:cart)로 같은 슬롯에 모은다. {} 안의 문자열만 해싱 대상이 된다.