캐시 스탬피드
캐시가 만료되는 순간 다수의 요청이 동시에 DB로 몰려 과부하가 발생하는 현상입니다.
발생 과정
정상 상황:
요청 → [캐시 HIT] → 즉시 응답
캐시 스탬피드:
캐시 만료
↓
요청 1 → 캐시 MISS → DB 조회 시작
요청 2 → 캐시 MISS → DB 조회 시작 ← 동시에 수백~수천 요청이
요청 3 → 캐시 MISS → DB 조회 시작 DB로 직접 몰림
...
↓
DB 과부하 → 응답 지연 → 장애
해결 방법
1. 뮤텍스 락 (Mutex Lock)
첫 번째 요청만 DB를 조회하고, 나머지는 대기합니다.
요청 1 → 캐시 MISS → 락 획득 → DB 조회 → 캐시 저장 → 락 해제
요청 2 → 캐시 MISS → 락 대기 --------→ 캐시 HIT → 응답 O
요청 3 → 캐시 MISS → 락 대기 --------→ 캐시 HIT → 응답 O
// Redis 분산 락 예시
if (redis.setnx("lock:key", "1")) { // 락 획득 시도
try {
String value = db.query(); // DB 조회
redis.set("key", value); // 캐시 저장
} finally {
redis.del("lock:key"); // 락 해제
}
} else {
Thread.sleep(100); // 잠시 대기 후 재시도
return redis.get("key");
}
2. PER 알고리즘 (확률적 조기 갱신)
만료 전에 확률적으로 미리 캐시를 갱신합니다.
// 남은 TTL이 적을수록 갱신 확률 증가
double ttl = redis.ttl("key");
double random = Math.random();
if (random * ttl < BETA) { // BETA: 갱신 민감도 상수
refreshCache(); // 만료 전 미리 갱신
}
3. TTL 분산
동일한 TTL로 인해 동시에 캐시가 만료되는 것을 방지합니다.
// 고정 TTL 대신 랜덤 편차 추가
int ttl = BASE_TTL + random.nextInt(60); // ex) 300 ± 60초
redis.setex("key", ttl, value);
4. 백그라운드 갱신
캐시 만료 전 별도 스레드로 미리 갱신합니다.
캐시 TTL: 10분
갱신 스케줄러: 9분마다 실행 → 항상 캐시 유지
해결 방법 비교
| 방법 | 장점 | 단점 |
|---|---|---|
| 뮤텍스 락 | DB 요청 1회만 보장 | 대기 시간 발생 |
| PER 알고리즘 | 자연스러운 갱신 | 구현 복잡 |
| TTL 분산 | 구현 단순 | 완전한 해결책 아님 |
| 백그라운드 갱신 | 만료 없이 유지 | 스케줄러 관리 필요 |
실무에서는 뮤텍스 락 + TTL 분산을 함께 적용하는 경우가 많습니다.