maxmemory 에 닿으면 새 데이터를 넣을 자리를 만들어야 한다. 무엇을 버릴지 고르는 규칙이 축출 정책이다.
| 정책 | 버리는 대상 |
|---|---|
noeviction | 아무것도 안 버리고 쓰기에 에러를 낸다 |
allkeys-lru | 전체 키 중 오래 안 쓴 것 |
allkeys-lfu | 전체 키 중 적게 쓴 것 |
volatile-lru / volatile-lfu | TTL 이 걸린 키 중에서만 위와 같이 |
allkeys-random | 무작위 |
volatile-ttl | 만료가 임박한 것부터 |
만료와 축출은 다르다
자주 섞이는데 별개다.
- 만료(expiration) — 내가 건 TTL 이 지나서 지워지는 것. 메모리와 무관하다
- 축출(eviction) — 메모리가 가득 차서 TTL 이 남아 있는 키까지 밀어내는 것
TTL 을 넉넉히 걸어 뒀는데 데이터가 일찍 사라진다면 대개 축출이다. 로그의 evicted_keys 지표를 봐야 구분된다.
LRU 와 LFU 는 무엇이 다른가
- LRU(Least Recently Used) — 마지막으로 언제 썼나. 최근에 쓰였으면 남긴다
- LFU(Least Frequently Used) — 얼마나 자주 쓰였나. 많이 쓰였으면 남긴다
배치 작업이 한밤중에 수만 건을 한 번씩 훑고 지나갔다고 하자. LRU 에게 그 키들은 전부 "방금 쓴 것" 이라 살아남고, 정작 종일 조회되던 인기 상품이 밀려난다. 아침에 적중률이 뚝 떨어져 있다. 이것이 캐시 오염이다.
LFU 는 접근 횟수를 보므로 한 번 훑고 지나간 키가 인기 항목을 밀어내지 못한다. 반대로 트렌드가 빠르게 바뀌는 데이터라면 LFU 가 과거의 인기에 매달려 새 항목을 못 들이기도 한다. Redis 의 LFU 는 카운터를 시간에 따라 감쇠시켜(lfu-decay-time) 이 문제를 줄인다.
정확한 LRU 가 아니다
정확한 LRU 는 모든 키를 접근 순서로 이어 둔 연결 리스트를 유지해야 한다. 키를 건드릴 때마다 그 노드를 맨 앞으로 옮겨야 하니, 포인터 메모리도 들고 갱신 비용도 든다. 키가 수천만 개면 그 자체가 부담이다.
Redis 는 대신 키를 몇 개 무작위로 뽑아 그중 가장 오래된 것을 버린다(maxmemory-samples, 기본 5). 표본을 늘리면 정확해지지만 CPU 를 더 쓴다. 정확도를 조금 내주고 비용을 크게 줄인 맞바꿈이고, 캐시라는 용도에서는 완벽한 LRU 가 필요하지 않다는 판단이 깔려 있다.
어느 것을 고르나
- 순수 캐시 —
allkeys-lru나allkeys-lfu. 지워져도 DB 에서 다시 채운다 - 세션·분산 락처럼 사라지면 안 되는 것이 섞여 있다 —
volatile-*로 두고 캐시성 키에만 TTL 을 건다. TTL 없는 키는 축출 대상에서 빠진다 noeviction은 저장소로 쓸 때다. 가득 차면 쓰기가 실패하므로 메모리 감시가 필수다
주의할 것은 volatile-* 인데 TTL 걸린 키가 하나도 없으면 버릴 대상이 없어 noeviction 처럼 에러가 난다는 점이다.
면접 함정
- ❌ "TTL 이 지나야 지워진다" → 메모리가 차면 TTL 이 남아 있어도 축출된다.
- ❌ "LFU 가 항상 낫다" → 접근 패턴이 빨리 변하면 LRU 가 유리하다.
- ❌ "Redis 의 LRU 는 정확하다" → 표본 기반 근사다.