데이터베이스 따라잡기

캐시와 DB

meellon 2026. 7. 6. 05:30

학습 목표

애플리케이션 캐시DB read 부하·connection·버퍼 풀 압력을 어떻게 줄이는지 설명할 수 있다.

Cache-aside·read-through·write-throughDB I/O·일관성 차이를 비교할 수 있다.

TTL·invalidation으로 stale read 통제할 수 있다.

Cache stampedeDB spike를 만드는 메커니즘완화를 설명할 수 있다.

DB = source of truth, 캐시 = 파생복제 lag·캐시 중첩 stale을 설명할 수 있다.

문제 상황

  • 상품 상세 API — PostgreSQL 동일 SELECT 매 요청 · p99 380ms
  • Redis TTL 5분 — 가격 UPDATE4분 구 가격 노출
  • 캐시 무효화 없이 DB만 수정 — 영구 stale
  • 프로모션 0시 — TTL 동시 만료 · DB connection pool 고갈
  • write-through 도입 — 쓰기 p99 2배 · 읽기만 캐시면 됐음
  • read replica + Redis — replica lag + cache TTL이중 stale

앞서 pool·ORM앱↔DB 연결·쿼리 최적화. 읽기를 DB 으로 빼는 캐시 계층과 DB 정합성을 본다.

1. DB 관점에서 캐시

캐시자주 읽고 상대적으로 잘 안 바뀌는 데이터를 빠른 store(Redis·in-memory)에 복사.

DB 없이 매번 캐시 hit 시
Parser·plan·buffer pool·disk 0 DB round-trip
Connection 점유 pool 여유
Replica·primary 부하 읽기 분산
Source of truth  =  PostgreSQL (primary)
Cache            =  disposable · rebuildable copy
Buffer pool      =  DBMS internal (앞서 디스크·페이지)
App cache        =  Redis / local — this episode
  • Architecture 캐싱 전략 — MSA·Redis 배치; 여기서는 DB 부하·일관성
  • NoSQL Redis — KV·TTL; RDB 앞 read-through 계층

2. Cache-aside (lazy loading)

Cache-aside이 cache·DB 순서 직접 제어. 가장 흔함.

Read

1. GET cache
2. hit   → return (0 DB query)
3. miss  → SELECT DB → SET cache + TTL → return
# code/cache-aside.example
def get_product(product_id: int):
    key = f"catalog:product:{product_id}"
    cached = redis.get(key)
    if cached:
        return json.loads(cached)

    row = db.execute(
        "SELECT id, name, price FROM products WHERE id = %s",
        (product_id,),
    ).fetchone()
    if row is None:
        return None

    payload = dict(row)
    redis.setex(key, 300, json.dumps(payload))
    return payload
  Hit Miss
DB 없음 1 SELECT
지연 sub-ms DB + cache write
위험 stale miss burst → DB spike

Write (일반)

1. UPDATE / DELETE in DB  (source of truth)
2. DEL cache key  (or version bump)
  • Write 후 cache update보다 delete 안전 — partial field 실수 방지
  • TX commit invalidation — rollback 시 cache 지우면 불필요 miss

3. Read-through · Write-through

패턴 DB 접점 일관성 적합
Cache-aside 앱이 각각 delete+TTL 관리 read-heavy 기본
Read-through miss 시 cache lib가 DB load lib 의존 표준 wrapper
Write-through write 동시 cache+DB read-after-write 쓰기 적고 즉시 read
Write-behind cache 먼저 · DB async 손실·순서 리스크 ingest burst
Cache-aside write:   COMMIT to PostgreSQL  →  DEL Redis key
Write-through:       app → cache layer writes DB + cache together
  • Write-through — DB write latency 그대로 + cache 오버헤드남용 지양
  • Write-behind — DB 장애유실원장 부적합

4. TTL과 invalidation

TTL자동 만료. invalidation 없어도 eventually 갱신.

  TTL only Delete on write
stale 최대 TTL commit 직후 없음
DB load 만료 spike write 마다 miss 가능
적합 비핵심 · 통계 가격·재고
# code/cache-invalidation.example
def update_product_price(product_id: int, new_price: decimal.Decimal):
    with db.transaction():
        db.execute(
            "UPDATE products SET price = %s WHERE id = %s",
            (new_price, product_id),
        )
    # after COMMIT
    redis.delete(f"catalog:product:{product_id}")
기법  
Jitter TTL ± random — 동시 만료 방지
Version in key product:42:v17immutable entry
CDC / outbox event consumer DELmulti-instance (CDC 편)
Negative cache 없는 id 짧은 TTL — 반복 SELECT 방지
  • Race — read miss가 commit 전 값 cache — 짧은 TTL + delete after commit
  • Multi-key — product 상세·목록·검색 동시tag invalidation

5. Cache stampede

Cache stampede대량 miss·TTL 동시 만료 → DB 동일 쿼리 폭주.

증상 DB에서
Redis hit rate 급락 동일 SELECT 수백
DB CPU 급등 buffer pool·lock
Pool 대기 앞서 pool exhaustion
# code/stampede-singleflight.example — one loader per key
_locks: dict[str, threading.Lock] = {}

def get_product_safe(product_id: int):
    key = f"catalog:product:{product_id}"
    cached = redis.get(key)
    if cached:
        return json.loads(cached)

    lock = _locks.setdefault(key, threading.Lock())
    with lock:  # singleflight
        cached = redis.get(key)  # double-check
        if cached:
            return json.loads(cached)
        row = load_from_db(product_id)
        redis.setex(key, 300 + random.randint(0, 30), json.dumps(row))
        return row
완화  
TTL jitter 만료 분산
Singleflight key당 1 loader
Probabilistic early refresh 만료 백그라운드 reload
Prewarm 배포·이벤트 cache 채움
Mutex + short wait miss 서기

6. DB · replica · cache 일관성

계층 역할 stale
Primary source of truth 없음 (TX 내)
Read replica read 분산 replication lag
Redis cache hot read TTL · invalidation 실패
Local (Caffeine) 초저지연 pod마다 다름
Worst case:  write primary  →  lag on replica  →  stale replica read
              invalidate miss  →  old value in Redis until TTL
규칙  
강한 read primary 또는 write-through
약한 read replica + cache · SLA 문서화
Invalidate primary commit 기준
Cache down fail open to DB · degraded 허용?
  • 앞서 replication lag — cache 없어도 stale · 겹치면 길어짐
  • 앞서 MVCC — cache는 스냅샷 고정 — DB 버전 넘어섬

7. 버퍼 풀 vs 앱 캐시

  Buffer pool (DBMS) App cache (Redis)
위치 PostgreSQL 내부 외부
단위 page object·JSON
일관 WAL·MVCC 책임
목적 disk I/O network·query
  • Buffer pool hit 높아도 parser·connection 남음app cache 여전히 유효
  • EXPLAIN (BUFFERS) — buffer vs app cache 별개 (실행 계획 편)

8. 체크리스트

항목 확인
Truth Primary only · cache 재구축 가능?
Pattern read-heavy → cache-aside?
Write COMMIT 후 DEL?
TTL jitter · 도메인문서?
Stampede singleflight · prewarm?
Replica cache miss → 어느 DB?
Fail Redis down → DB direct OK?
Metric hit rate · miss latency · DB QPS 상관

9. 정리

  • 캐시 — DB read·connection 보호파생 복사
  • Cache-aside — hit 0 query · miss 1 SELECTdelete on write
  • Write-through일관 ↑ · DB write 비용
  • TTL + invalidationstale window 명시
  • StampedeDB spike — jitter·singleflight
  • Primary = truth · replica·cache lag 겹침 주의

다음에 다룰 것

  • CDC와 이벤트
  • Debezium, outbox, 이벤트 순서

해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan), Designing Data-Intensive Applications (Martin Kleppmann), Redis 공식 문서 의 내용을 기반으로 합니다.

'데이터베이스 따라잡기' 카테고리의 다른 글

슬로우 쿼리 진단  (0) 2026.07.08
CDC와 이벤트  (0) 2026.07.07
Connection Pool과 ORM  (0) 2026.07.05
NoSQL 개요  (0) 2026.07.04
분산 트랜잭션 개요  (0) 2026.07.03