Zettelkasten

Redis KEYS와 SCAN은 둘 다 keyspace bucket을 순회하며 차이는 한 호흡에 보느냐 커서로 잘라서 보느냐다

·수정 1

요약

  • Redis의 KEYS와 SCAN은 같은 일(전체 keyspace bucket 순회) 을 한다. 차이는 한 명령으로 끝내느냐, 커서로 잘게 쪼개느냐일 뿐 총 작업량은 거의 같다.
  • 흔히 "정확도 vs 성능"의 선택으로 오해하지만, 실제 트레이드오프는 내 응답 속도 vs 전체 시스템 가용성이다.

본문

왜 둘 다 "순회"해야 하는가

Redis의 모든 키는 하나의 거대한 해시 테이블(dict)에 들어있다. 해시는 정확한 키 하나(GET stats:app:1:...)를 O(1)로 꺼낼 수 있을 뿐이고, stats:* 같은 패턴에 대한 인덱스는 존재하지 않는다.

그래서 매칭되는 키를 찾으려면 bucket 0번부터 N번까지 전부 열어보고 안에 든 키 이름을 패턴과 비교하는 수밖에 없다. 이 행위 자체가 "순회"다.

KEYS와 SCAN의 차이

KEYS SCAN
한 명령에서 보는 bucket 전체 (0 ~ N) 일부 (COUNT 힌트 기준)
Redis 서버 점유 끝까지 독점 매 호출 짧게 점유, 사이에 다른 명령 끼어들 수 있음
커서 없음 "다음에 시작할 bucket 위치"를 클라이언트가 들고 다님
총 작업량 동일 동일 (커서 관리 오버헤드로 오히려 살짝 더 많음)

Redis는 명령 실행이 단일 스레드라서 KEYS가 2초 걸리면 그 2초 동안 다른 모든 incr/get이 대기한다. 1억 imp/일 트래픽이라면 리포트 호출 한 번이 수집 파이프라인 전체를 멈출 수 있다.

"결국 모든 키가 필요한데 정확도 보장 안 되는 거 아냐?" 라는 오해

SCAN의 weak guarantee는 "어떤 존재하는 키가 반환되느냐" 에 대한 보장이지 "존재하지 않는 키를 만들어내느냐" 가 아니다.

  • 순회 내내 살아있던 키: 반환 보장
  • 순회 중에 새로 생긴 키: 반환될 수도, 안 될 수도
  • 존재한 적 없는 키: 절대 반환 안 함

실시간 집계 시스템에서 "원자적 스냅샷"은 어차피 환상이다. KEYS 결과도 클라이언트에 응답이 도착할 즈음이면 이미 stale이다. SCAN이 놓친 키 1~2개는 다음 폴링 사이클에서 잡히면 그만이다.

도서관 비유

제목 색인이 없는 도서관에서 "제목에 '레디스'가 들어간 책 다 찾기":

  • KEYS — 사서가 카운터를 닫고 1번 책장부터 1000번 책장까지 쭉 보고 결과를 한 번에 준다. 그동안 다른 손님은 책을 빌릴 수 없다.
  • SCAN — 사서가 "지금 1~50번 책장 봤고 매칭 3권, 다음엔 51번부터 보세요"라는 영수증(커서)을 주고 다음 손님을 응대한다. 다음 호출에 영수증을 주면 51번부터 이어서 본다.

같은 도서관 전체를 본다는 점은 똑같다. 나눠서 보냐, 한 번에 보냐의 차이.

진짜 패턴 매칭을 빠르게 하려면

SCAN도 결국 전체 keyspace를 다 본다. 패턴 매칭 자체를 빠르게 만들고 싶으면 SCAN도 답이 아니다. Set이나 Hash로 키 인덱스를 직접 관리해야 한다.

SADD index:campaign:10:date:2024-03-21  stats:app:1:campaign:10:...
SADD index:campaign:10:date:2024-03-21  stats:app:2:campaign:10:...

리포트 시: SMEMBERS index:campaign:10:date:2024-03-21 → O(matched). bucket 순회 자체가 사라진다.

위치

  • KEYS는 운영 코드에 쓰지 말 것 — Redis 공식 docs도 "production environments에서 사용 주의, 디버깅용"으로 명시
  • 운영에서 키 순회가 꼭 필요하면 SCAN (redis-py는 scan_iter(match=..., count=...))
  • 패턴 매칭이 hot path라면 keyspace 인덱스(Set/Hash)를 직접 설계

참고