Redis Cache Stampede 현상 극복기: 캐시 적용하면 DB 부하 끝 아닌가?

캐시 적용하면 DB 부하 끝 아닌가?

 

서론

 

Redis 캐시를 도입할 때, 이런 생각이 들었다.

 

“조회 성능? 인기 데이터 레디스에 캐싱하고 만료시간(TTL) 걸어두면 DB 부하 끝 아닌가?”

 

그렇게 생각하고 메인 페이지 데이터 캐시 만료시간을 10분으로 세팅했다.

하지만 10분이 지나 캐시가 만료되는 순간, 캐시 만료 직후 동시 요청이 DB 조회로 몰리는 Cache Stampede 현상을 경험했다.

 

이 글은 캐시 만료 시 발생하는 폭주 현상과 이를 해결한 Mutex Lock 적용기를 다루고 있다.

부족한 내용이 있을 수 있지만, 나처럼 막 시작한 사람에게는 도움이 될 거라 생각한다.

 


 

📚 Cache Stampede 해결 기법

 

Cache Stampede는 고빈도 캐시 키가 만료되었을 때 수많은 요청이 동시에 DB 조회를 수행하며 무너지는 현상입니다.

 


 

해결책

 

1. Mutex Lock (락 선점 캐싱)

  • 캐시가 만료되었을 때, 딱 1개 세션만 락을 얻어 DB를 조회하고 캐시를 갱신함
  • 나머지 요청은 락이 풀릴 때까지 잠시 대기 후 갱신된 캐시를 읽어옴

아래 코드는 핵심 안전 조건을 보여주는 개념 예시입니다. 락 값에는 요청별 임의 토큰을 사용하고, 제한된 재시도 후에도 값을 얻지 못하면 호출자에게 실패를 반환해야 합니다. 락 해제는 현재 값이 내 토큰과 같을 때만 삭제하는 원자 연산으로 처리합니다.

public String getPopularData(String key) {
    String cached = redis.get(key);
    if (cached != null) return cached;

    String lockKey = "lock:" + key;
    String token = UUID.randomUUID().toString();
    boolean acquired = redis.setIfAbsent(lockKey, token, Duration.ofSeconds(3));

    if (acquired) {
        try {
            cached = redis.get(key); // double check after acquiring the lock
            if (cached != null) return cached;
            String loaded = db.fetchData();
            redis.set(key, loaded, Duration.ofMinutes(10));
            return loaded;
        } finally {
            redis.deleteOnlyWhenValueMatches(lockKey, token); // Lua or DELEX
        }
    }

    for (int attempt = 0; attempt < 5; attempt++) {
        sleepWithJitter(attempt);
        cached = redis.get(key);
        if (cached != null) return cached;
    }
    throw new CacheRefreshInProgressException(key);
}

 


 

정리하며

 

  • 캐시 만료 순간의 몰림 현상(Stampede)은 무서운 시스템 장애를 유발할 수 있다.
  • Mutex 락이나 확률적 미리 갱신(PER) 알고리즘을 사용하면 DB 부하 급증을 완화할 수 있지만, TTL·재시도·장애 시나리오를 부하 테스트로 검증해야 한다.

 

대용량 캐싱의 오묘한 비하인드를 이해하고 안정성을 챙기게 된 뜻깊은 계기였다.

공식 문서와 추가 확인

락 소유권과 안전한 해제 조건은 Redis 공식 Distributed Locks 문서를 기준으로 다시 확인했습니다.

댓글 남기기