최근 면접에서 Redis에서 Atomic한 연산을 어떻게 처리하는지에 대한 질문을 받았는데, Cluster 환경에서 Lua 스크립트의 제약을 떠올리지 못해 제대로 답변하지 못했습니다. 그래서 정리해보려 합니다.
Redis 클러스터는 Redis가 "자동 샤딩과 고가용성(HA)"을 제공하기 위해 만든 분산 구조로, 여러 노드를 하나의 논리적 Redis처럼 사용할 수 있게 해줍니다. 키 공간을 여러 노드에 나눠 담으면서 일부 노드에 장애가 발생해도 전체 클러스터가 계속 동작하도록 설계되어 있습니다.
값을 저장할 때는 키를 해시하여 0~16383 사이의 해시 슬롯을 구하고, 해당 슬롯을 담당하는 노드로 요청이 라우팅됩니다. 클러스터에서는 노드 간 gossip 프로토콜을 통해 상태와 슬롯 정보를 주고받으면서, 마스터 장애가 감지되면 해당 마스터의 replica를 새 마스터로 승격하는 자동 장애 조치를 수행합니다.
Redis 클러스터의 장점과 제약은 다음과 같습니다.
Redis Lua 스크립트는 여러 Redis 명령을 하나의 서버 사이드 스크립트로 묶어 한 번에 원자적으로 실행하기 위한 기능입니다. 즉, 클라이언트에서 여러 명령을 여러 번 호출하는 대신, Lua 코드를 EVAL/EVALSHA 명령으로 실행해 복잡한 로직을 한 번에 처리할 수 있습니다.
Lua 스크립트는 보통 EVAL "script ..." numkeys key1 key2 ... arg1 arg2 ... 형식으로 호출하며, 이때 KEYS는 스크립트가 접근할 키 목록, ARGV는 일반 인자를 담는 배열입니다. 같은 스크립트를 반복적으로 사용할 경우에는 SCRIPT LOAD로 서버에 등록한 뒤, 반환된 SHA1 해시를 이용해 EVALSHA로 호출하면 파싱·전송 비용을 줄일 수 있습니다.
Lua 스크립트를 사용하는 이유는 다음과 같습니다.
이러한 특성 때문에 Lua 스크립트는 여러 키를 동시에 읽고 수정해야 하는 레이트 리미터, 분산 락, 재고 차감과 같은 시나리오에서 자주 사용됩니다.
Redis 클러스터에서 Lua 스크립트를 사용할 때는 "모든 키를 한 슬롯(한 노드)에 모이게 설계하는 것"과 "노드 블로킹·메모리·플래그 제약"을 특히 주의해야 합니다.
Lua 스크립트가 접근하는 모든 키는 동일한 해시 슬롯, 즉 동일한 노드에 위치해야 합니다. 서로 다른 슬롯에 있는 키들을 대상으로 스크립트를 실행하면 CROSSSLOT 오류가 발생하고, 스크립트는 실행되지 않습니다.
관련 키들을 같은 노드에 모으려면 해시 태그를 사용합니다. 해시 태그는 키 이름에서 중괄호({…})로 감싼 부분만 해싱에 사용하도록 만드는 기능으로, 이 범위가 동일하면 같은 슬롯으로 매핑됩니다. 이를 활용하면 서로 연관된 키들을 의도적으로 동일한 노드에 배치할 수 있습니다.
예를 들어 Spring Cloud Gateway의 RedisRateLimiter는 다음과 같이 해시 태그를 사용해 토큰 버킷 관련 키들을 한 노드에 모읍니다.
static List<String> getKeys(String id, String routeId) {
// use `{}` around keys to use Redis Key hash tags
// this allows for using redis cluster
// Make a unique key per user and route.
String prefix = "request_rate_limiter.{" + routeId + "." + id + "}.";
// You need two Redis keys for Token Bucket.
String tokenKey = prefix + "tokens";
String timestampKey = prefix + "timestamp";
return Arrays.asList(tokenKey, timestampKey);
}
routeId와 id를 중괄호 안에 넣어 {routeId.id}를 해시 태그로 사용함으로써, .tokens와 .timestamp 키가 항상 같은 해시 슬롯으로 매핑되고 같은 노드에서 처리됩니다. 덕분에 동일 사용자,라우트 조합에 대한 레이트 리미팅 로직을 Redis 클러스터 환경에서도 한 노드에서 원자적으로 실행할 수 있습니다.
Redis 클러스터의 각 노드는 단일 스레드 이벤트 루프로 동작하므로, 하나의 Lua 스크립트가 실행되는 동안 해당 노드의 다른 요청 처리가 모두 지연됩니다. 따라서 스크립트 안에서 큰 컬렉션을 한 번에 가져와 처리하는 작업은 피하고, CPU 바운드나 I/O 바운드 로직은 가능하면 배치 작업이나 애플리케이션 레벨로 분리하는 것이 좋습니다.