Redis 캐시 스탬피드 방지: 분산 락과 Jitter 활용 가이드

유명 연예인의 한정판 굿즈 판매가 시작되는 순간, 수십만 명의 사용자가 동시에 접속합니다. 이때 Redis에 저장된 상품 재고 데이터의 만료 시간이 하필 지금 끝난다면 어떤 일이 벌어질까요? 모든 요청이 한꺼번에 데이터베이스(DB)로 몰리며 서버가 마비되는 '캐시 스탬피드(Cache Stampede)' 현상이 발생합니다.

이 글에서는 수석 엔지니어의 관점에서 대용량 트래픽 환경의 안정성을 보장하기 위한 분산 락(Distributed Lock) 처리와 Jitter(난수 지연) 기법을 실무 코드로 설명합니다.

TL;DR — 캐시 스탬피드는 특정 키 만료 시 대량의 요청이 DB로 몰리는 현상이며, 분산 락으로 DB 접근을 제어하고 Jitter로 만료 시간을 분산하여 해결합니다.

1. 캐시 스탬피드란 무엇인가

💡 비유로 이해하기: 맛집 앞에 1,000명이 줄을 서 있는데, 가게 문이 잠겼습니다. 사장님이 재료를 구하러 간 사이(Cache Miss), 1,000명이 동시에 사장님 휴대폰으로 전화를 거는 상황과 같습니다. 사장님의 휴대폰(DB)은 즉시 먹통이 됩니다.

캐시 스탬피드는 '중복 읽기(Duplicate Read)'와 '중복 쓰기(Duplicate Write)'가 동시에 폭발하는 현상입니다. Redis의 특정 키가 만료(TTL)되는 순간, 해당 데이터를 필요로 하는 수천 개의 애플리케이션 스레드가 캐시 미스를 감지하고 동시에 DB에 쿼리를 날립니다. 최신 버전 Redis 7.x 환경에서도 애플리케이션 레벨의 처리가 없으면 DB 커넥션 풀이 순식간에 고갈됩니다.

기존 방식은 단순히 캐시가 없으면 DB를 조회하고 다시 캐시에 쓰는 순차적 구조를 가집니다. 하지만 트래픽이 임계치를 넘으면 DB 응답 속도가 느려지고, 그사이 더 많은 요청이 DB로 몰리는 악순환(Thundering Herd)에 빠집니다.

2. 실무에서 필요한 이유

프로모션 이벤트나 실시간 검색어 순위처럼 특정 데이터(Hot Key)에 접근이 집중될 때 필수입니다. 캐시가 만료된 10ms 사이의 찰나에 수천 건의 쿼리가 발생하면 DB 인스턴스의 CPU 점유율이 100%를 찍으며 전체 시스템 장애로 이어집니다.

또한, MSA(Microservices Architecture) 환경에서 여러 서비스가 동일한 Redis를 공유할 때, 한 서비스의 캐시 스탬피드가 다른 서비스의 가용성까지 해치는 연쇄 장애(Cascading Failure)를 방지하기 위해 이 기법들을 반드시 적용해야 합니다.

3. 단계별 구현 가이드

가장 효과적인 방법은 분산 락으로 DB 접근 권한을 1개로 제한하고, Jitter로 만료 시점을 흩어놓는 것입니다.

Step 1. Redisson을 이용한 분산 락 환경 설정

자바 환경에서는 Redisson 라이브러리를 사용해 간단하게 분산 락을 구현합니다. Lettuce보다 Pub/Sub 기반의 락 획득 메커니즘이 효율적입니다.

// RedissonConfig.java
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

Step 2. Double-Check Locking 패턴 적용

락을 획득한 직후에 다시 한번 캐시를 확인하는 것이 핵심입니다. 먼저 들어온 스레드가 이미 캐시를 갱신했을 수 있기 때문입니다.

public String getDataWithLock(String key) {
    String value = redisTemplate.opsForValue().get(key);
    if (value != null) return value; // 캐시 히트

    RLock lock = redisson.getLock("lock:" + key);
    try {
        // 5초 동안 대기, 10초 동안 락 점유
        if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
            // 락 획득 후 다시 캐시 확인 (Double-Check)
            value = redisTemplate.opsForValue().get(key);
            if (value != null) return value;

            value = db.fetchData(key); // DB 조회
            
            // Jitter 적용하여 저장
            long ttl = 3600 + new Random().nextInt(300); // 1시간 + 최대 5분 무작위
            redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) lock.unlock();
    }
    return value;
}

Step 3. 검증 및 결과 확인

로그를 통해 동일 키에 대해 단 한 번의 DB 쿼리만 발생하는지 확인합니다. Jitter가 적용된 데이터들의 TTL을 PTTL 명령어로 조회했을 때 각각의 만료 시간이 다른지 체크합니다.

# Redis CLI 확인
$ redis-cli pttl product:123
(integer) 3654000
$ redis-cli pttl product:124
(integer) 3812000

4. 분산 락 vs. Jitter 비교

상황에 따라 두 기법을 적절히 혼합하거나 하나만 선택해야 합니다.

  • 유지보수
  • 기준분산 락 (Distributed Lock)Jitter (난수 지연)
    성능 영향락 획득 대기로 인한 오버헤드 발생거의 없음
    복잡도높음 (락 획득/해제 로직 필요)낮음 (랜덤 값 추가)
    Redisson 등 라이브러리 관리 필요비즈니스 로직만 수정
    적합 규모초고농축 Hot Key 보호 시 필수대량의 다양한 키 만료 분산 시 유리

    트래픽이 극도로 몰리는 키는 분산 락이 필수이며, 수만 개의 일반 키가 동시에 만료되는 것을 막으려면 Jitter가 가장 효율적입니다.

    5. 주의사항

    ⚠️ 가장 자주 하는 실수: 락 해제(Unlock) 로직을 `finally` 블록에 넣지 않으면, DB 장애 발생 시 락이 영원히 풀리지 않아 시스템 전체가 마비됩니다.

    또한, 락의 만료 시간(Lease Time)을 너무 짧게 잡으면 DB 조회가 끝나기 전에 락이 풀려 다른 스레드가 다시 DB에 접근하는 현상이 발생합니다. 반대로 너무 길면 프로세스가 죽었을 때 해당 키에 대한 갱신이 한참 동안 지연됩니다.

    에러 메시지별 해결법

    // 에러: RedissonLock - Unable to acquire lock
    // 원인: 대기 시간(Wait Time) 초과
    // 해결: waitTime을 DB 평균 응답 시간의 2배 이상으로 설정

    6. 실전 운영 팁

    Jitter의 범위는 전체 TTL의 5%~10% 정도로 설정하는 것이 좋습니다. 예를 들어 1시간 만료라면 3~6분 사이의 난수를 더해줍니다. 너무 큰 Jitter는 캐시 효율을 떨어뜨립니다.

    확률적 만료(PER, Probabilistic Early Recomputation) 알고리즘을 사용하면 락 없이도 효과적으로 스탬피드를 방지할 수 있습니다. 이는 만료 시간이 되기 직전에 확률적으로 미리 갱신을 수행하는 기법입니다.

    📌 핵심 요약

    • 캐시 스탬피드는 DB를 죽이는 치명적인 '떼거리' 요청 현상이다.
    • 분산 락을 통해 딱 하나의 스레드만 DB에 접근하게 제어한다.
    • Jitter를 적용해 수많은 캐시 키가 한 번에 만료되지 않도록 분산한다.

    Frequently Asked Questions

    Q. 분산 락을 쓰면 API 응답 속도가 느려지지 않나요?

    A. 첫 번째 스레드 외에는 대기가 발생하지만, DB 다운으로 인한 무한 대기보다는 훨씬 빠르고 안전합니다.

    Q. Jitter는 무조건 써야 하나요?

    A. 배치 작업으로 수만 개의 키를 동시에 생성하는 경우라면 필수적으로 적용해야 장애를 막습니다.

    Q. Redis 자체가 죽으면 어떻게 대응하나요?

    A. Circuit Breaker를 연동해 Redis 장애 시 DB로 직접 가되, 처리율 제한(Rate Limiting)을 반드시 병행해야 합니다.

    Post a Comment