Redis에서 대규모 객체 삭제가 느린 이유에 대해 자세히 알아보려면 관련 개요 문서를 참고하세요.
Redis에서 대규모 세트(Set)를 삭제할 때는 DEL 명령어를 직접 실행하면 안 됩니다. 키에 수백만 개의 멤버가 저장되어 있다면, Redis는 단일 스레드로 동작하기 때문에 삭제가 완료될 때까지 다른 모든 명령이 차단(blocking)되어 서비스 전반에 심각한 지연이 발생할 수 있습니다.
이를 방지하려면 다음 두 단계로 나누어 삭제를 진행해야 합니다:
키 이름 변경(Rename): 먼저 해당 키를 고유한 네임스페이스가 포함된 새 이름으로 변경합니다. 이렇게 하면 다른 Redis 클라이언트 입장에서는 해당 세트가 즉시 "삭제된 것"처럼 보이므로, 실제 데이터 정리 작업과 무관하게 애플리케이션 로직이 정상적으로 동작합니다.
점진적 배치 삭제(Incremental Delete): 세트의 멤버들을 작은 단위의 배치(batch)로 나누어 조금씩 삭제하고, 세트가 완전히 비워질 때까지 반복합니다. 삭제 명령 하나당 처리량을 제한함으로써 Redis 서버가 오랫동안 블로킹되지 않도록 보장할 수 있습니다.
주의: 아래 코드는 Redis 연결 실패 상황을 우아하게 처리하지 않습니다. Redis 명령이 실패하여 예외(exception)가 발생하면, 남은 잔여 키를 수동으로 정리해야 합니다. 운영 환경에서는 재시도(retry) 로직과 예외 처리를 함께 구현하는 것이 좋습니다.
의사 코드(Pseudo-code)
# 키 이름 변경
newkey = "gc:hashes:" + redis.INCR("gc:index")
redis.RENAME("my.set.key", newkey)
# 100개씩 배치로 세트 멤버 삭제
cursor = 0
loop
cursor, members = redis.SSCAN(newkey, cursor, "COUNT", 100)
if size of members > 0
redis.SREM(newkey, members)
end
if cursor == 0
break
end
end
동작 원리
SSCAN은 커서(cursor) 기반으로 세트를 순회하는 명령어입니다. 한 번의 호출로 전체 컬렉션을 스캔하지 않고, COUNT 옵션으로 지정한 개수만큼씩 나누어 조회합니다. 반환된 커서 값이 0이 되면 모든 요소의 순회가 완료된 것이므로 루프를 종료하면 됩니다. 각 순회마다 조회된 멤버를 SREM으로 제거하는 방식입니다.
Ruby 구현 예제
$redis = Redis.new
def delete_set(key)
# 키 이름 변경
newkey = "gc:sets:#{$redis.incr("gc:index")}"
$redis.rename(key, newkey)
# 100개씩 배치로 세트 멤버 삭제
cursor = "0"
loop do
cursor, members = $redis.sscan(newkey, cursor, count: 100)
$redis.srem(newkey, members) if members.size > 0
break if cursor == "0"
end
end
# 사용 예:
#
# delete_set("my.large.set")
위 로직을 Ruby의 백그라운드 잡(background job)으로 처리하는 대표적인 구현 예시는 다음과 같습니다:
- Resque — Redis 기반 큐를 사용하는 백그라운드 잡 라이브러리
- Sidekiq — 고성능 스레드 기반 백그라운드 처리 프레임워크
대규모 세트 삭제 작업을 백그라운드 잡으로 위임하면 웹 요청 응답 시간에 영향을 주지 않으면서 안전하게 데이터를 정리할 수 있습니다.
← "Redis에서 대규모 객체 삭제"로 돌아가기