Workspace IndexAlgorithms › Day 79

Cache Consistency, Invalidation, and Stampede Prevention TODO

Algorithms · Day 79 / 100 · E. Data & Storage Engines (Day 69-81)

Concept

A cache starts carrying a consistency problem the moment the source of truth and the copy diverge; policies broadly split into expiration-based (TTL) and invalidation-based (delete or update on write). Updating the cache on the write path can let concurrent writes land out of order and leave a stale value behind, so deleting is usually safer than updating. A cache stampede happens when a popular key expires and many requests hit the origin simultaneously, which can momentarily take the origin down. The countermeasures are single-flight (letting only one request query the origin), adding random jitter to expiration times, proactive early recomputation before expiry, and stale-while-revalidate (serving the stale value briefly while a refresh is in flight). Whichever policy you pick, you first have to define 'how much staleness is acceptable' before a choice is even possible.

A large share of outages don't come from missing a cache — they come from the cache emptying all at once and the backend collapsing under the exposed load.

Code & Formula

# 캐시 일관성·무효화·스탬피드 방지 — TTL 만료 순간 다수 요청이 몰리는 스탬피드를
# single-flight(락)로 한 요청만 원본을 조회하게 막는 것을 시연한다.

import time
import threading

origin_calls = 0
origin_lock = threading.Lock()

def slow_origin_fetch(key):
    global origin_calls
    with origin_lock:
        origin_calls += 1
    time.sleep(0.05)  # 원본 DB/서비스 호출을 흉내
    return f"value-for-{key}"

class SingleFlightCache:
    def __init__(self):
        self.store = {}       # key -> (value, expires_at)
        self.inflight = {}    # key -> threading.Event (동시 요청 합류용)
        self.lock = threading.Lock()

    def get(self, key, ttl=1.0):
        with self.lock:
            entry = self.store.get(key)
            if entry and entry[1] > time.time():
                return entry[0]
            if key in self.inflight:
                event = self.inflight[key]
            else:
                event = threading.Event()
                self.inflight[key] = event
                event = None  # 이 스레드가 원본을 조회할 담당자
        if event is not None:
            event.wait()
            return self.store[key][0]
        value = slow_origin_fetch(key)
        with self.lock:
            self.store[key] = (value, time.time() + ttl)
            waiter = self.inflight.pop(key)
            waiter.set()
        return value

cache = SingleFlightCache()
results = []
def worker():
    results.append(cache.get("hot-key"))

threads = [threading.Thread(target=worker) for _ in range(20)]
for t in threads: t.start()
for t in threads: t.join()

print("concurrent requests:", len(threads))
print("origin fetches actually made:", origin_calls)  # 1 이어야 stampede 방지 성공
print("all results identical:", len(set(results)) == 1)

Exercise

Build a load test that hits the same key concurrently, then compare origin query counts and p99 latency between a plain TTL cache and one using single-flight.

Practical Connection

Values like prices, order-book snapshots, or oracle responses — where the origin is expensive and access is concentrated — carry the highest stampede risk, and here 'acceptable staleness' maps directly to the price accuracy users actually see.

Where it lands in Jayverse

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

Expression뜻 · 쓰이는 자리
cache stampede캐시 쏠림(인기 키 만료 시 요청이 한꺼번에 몰리는 현상) · 캐시 장애의 대표적 원인. "A cache stampede happens when a popular key expires"
single-flight(동시 요청을) 하나로 묶어 처리하는 기법 · 중복 요청 중 하나만 원본에 보내는 방어 기법. "single-flight (letting only one request query the origin)"
jitter지터(무작위로 섞은 편차) · 만료 시각을 흩뿌려 동시 만료를 막는 기법. "adding random jitter to expiration times"
stale-while-revalidate갱신 중에는 오래된 값을 잠시 보여주는 기법 · 최신화 동안 지연을 감추는 캐시 전략. "stale-while-revalidate (serving the stale value briefly"
take down(서버 등을) 다운시키다·마비시키다 · 몰린 요청이 원본 서버를 멈추게 하는 상황. "can momentarily take the origin down"
exposed load(캐시가 사라져) 그대로 노출된 부하 · 백엔드가 감당 못 하는 트래픽을 가리킴. "the backend collapsing under the exposed load"
acceptable staleness허용 가능한 데이터 지연(오래됨) 정도 · 캐시 전략을 고르기 전에 먼저 정의해야 하는 기준. "how much staleness is acceptable"
TTL생존 시간(Time To Live) · 캐시 정책 중 만료 시간 기반 방식을 가리킴. "policies broadly split into expiration-based (TTL)"

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

캐시 일관성·무효화·스탬피드 방지 TODO

Algorithms · Day 79 / 100 · E. 데이터·스토리지 엔진 (Day 69–81)

개념

캐시는 원본과 사본이 갈라지는 순간부터 일관성 문제를 안고 시작하며, 정책은 크게 만료 기반(TTL)과 무효화 기반(쓰기 시 삭제·갱신)으로 나뉜다. 쓰기 경로에서 캐시를 갱신하면 동시 쓰기 순서가 뒤바뀌어 오래된 값이 남을 수 있어, 보통은 갱신보다 삭제가 더 안전하다. 캐시 스탬피드는 인기 키가 만료되는 순간 다수 요청이 동시에 원본으로 몰리는 현상으로, 원본이 순간적으로 무너질 수 있다. 대응은 단일 비행(single-flight)으로 한 요청만 원본을 조회하게 하거나, 만료 시각에 무작위 지터를 주거나, 만료 전에 미리 갱신하는 조기 재계산, 그리고 갱신 중 낡은 값을 잠시 제공하는 stale-while-revalidate이다. 어떤 정책이든 "허용 가능한 낡음의 정도"를 먼저 정의해야 선택이 가능하다.

장애의 상당수는 캐시가 없어서가 아니라 캐시가 한꺼번에 비면서 뒤쪽 시스템이 무너지는 형태로 발생한다.

코드 · 수식

# 캐시 일관성·무효화·스탬피드 방지 — TTL 만료 순간 다수 요청이 몰리는 스탬피드를
# single-flight(락)로 한 요청만 원본을 조회하게 막는 것을 시연한다.

import time
import threading

origin_calls = 0
origin_lock = threading.Lock()

def slow_origin_fetch(key):
    global origin_calls
    with origin_lock:
        origin_calls += 1
    time.sleep(0.05)  # 원본 DB/서비스 호출을 흉내
    return f"value-for-{key}"

class SingleFlightCache:
    def __init__(self):
        self.store = {}       # key -> (value, expires_at)
        self.inflight = {}    # key -> threading.Event (동시 요청 합류용)
        self.lock = threading.Lock()

    def get(self, key, ttl=1.0):
        with self.lock:
            entry = self.store.get(key)
            if entry and entry[1] > time.time():
                return entry[0]
            if key in self.inflight:
                event = self.inflight[key]
            else:
                event = threading.Event()
                self.inflight[key] = event
                event = None  # 이 스레드가 원본을 조회할 담당자
        if event is not None:
            event.wait()
            return self.store[key][0]
        value = slow_origin_fetch(key)
        with self.lock:
            self.store[key] = (value, time.time() + ttl)
            waiter = self.inflight.pop(key)
            waiter.set()
        return value

cache = SingleFlightCache()
results = []
def worker():
    results.append(cache.get("hot-key"))

threads = [threading.Thread(target=worker) for _ in range(20)]
for t in threads: t.start()
for t in threads: t.join()

print("concurrent requests:", len(threads))
print("origin fetches actually made:", origin_calls)  # 1 이어야 stampede 방지 성공
print("all results identical:", len(set(results)) == 1)

연습

동일 키를 동시에 조회하는 부하 테스트를 만들어, 단순 TTL 캐시와 single-flight을 적용한 캐시의 원본 조회 횟수와 p99를 비교해 보기.

실무 · Verex 연결

가격·오더북 스냅샷·오라클 응답처럼 원본이 비싸고 접근이 몰리는 값일수록 스탬피드 위험이 크고, 여기서 허용 가능한 낡음은 곧 사용자에게 보이는 가격 정확도와 직결된다.

Jayverse에서의 위치

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

Expression뜻 · 쓰이는 자리
cache stampede캐시 쏠림(인기 키 만료 시 요청이 한꺼번에 몰리는 현상) · 캐시 장애의 대표적 원인. "A cache stampede happens when a popular key expires"
single-flight(동시 요청을) 하나로 묶어 처리하는 기법 · 중복 요청 중 하나만 원본에 보내는 방어 기법. "single-flight (letting only one request query the origin)"
jitter지터(무작위로 섞은 편차) · 만료 시각을 흩뿌려 동시 만료를 막는 기법. "adding random jitter to expiration times"
stale-while-revalidate갱신 중에는 오래된 값을 잠시 보여주는 기법 · 최신화 동안 지연을 감추는 캐시 전략. "stale-while-revalidate (serving the stale value briefly"
take down(서버 등을) 다운시키다·마비시키다 · 몰린 요청이 원본 서버를 멈추게 하는 상황. "can momentarily take the origin down"
exposed load(캐시가 사라져) 그대로 노출된 부하 · 백엔드가 감당 못 하는 트래픽을 가리킴. "the backend collapsing under the exposed load"
acceptable staleness허용 가능한 데이터 지연(오래됨) 정도 · 캐시 전략을 고르기 전에 먼저 정의해야 하는 기준. "how much staleness is acceptable"
TTL생존 시간(Time To Live) · 캐시 정책 중 만료 시간 기반 방식을 가리킴. "policies broadly split into expiration-based (TTL)"

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 1131. 벡터 DB와 ANN 인덱스(HNSW·IVF-PQ)1133. 외부 정렬·병합 전략과 병렬 정렬 (TAOCP 3권) →