Skip to content

บทที่ 8 — Distributed Caching

← บทที่ 7 | สารบัญ | บทที่ 9 →

หลังจบบท คุณจะ:

  • เข้าใจว่าทำไม cache สำคัญ + ทำงานยังไง
  • เลือก caching pattern ที่เหมาะกับ workload — 4 patterns หลักสรุปสั้น ๆ ก่อน:
Patternใครเขียน cache?ใครอ่าน DB ตอน miss?
Cache-aside (lazy loading)app จัดการเองapp
Read-throughcache library จัดการcache (ผ่าน loader)
Write-throughapp เขียน cache → cache เขียน DB sync
Write-back (write-behind)app เขียน cache, cache flush DB async ทีหลัง
  • จัดการ cache invalidation + cache stampede
  • รู้จัก distributed cache (Redis Cluster, Memcached) + CDN
  • เข้าใจ TTL strategy, sharding, replication ของ cache

1. ทำไม Cache สำคัญ — พลังของ "เก็บไว้ใกล้ ๆ"

ลองนึกถึงร้านกาแฟ — ถ้ารับออเดอร์ทุกแก้วต้องเดินไปต้มน้ำใหม่ทั้งหมด → ลูกค้ารอนาน ถ้ามีน้ำร้อนเตรียมไว้ในกระติก → ทำกาแฟได้ใน 30 วินาที

ใน software เหมือนกัน — cache คือ "กระติกน้ำร้อน" ของระบบ

ตัวเลขที่ต้องจำ (จากบท 0)

text
L1 cache:                   0.5 ns       ← เร็วที่สุด
RAM:                        100 ns       ← เร็วมาก
SSD (4KB) NVMe:             10-30 μs     ← 100-300x ช้ากว่า RAM
SSD (4KB) SATA:             100-150 μs   ← ~1000-1500x ช้ากว่า RAM
Network (same DC):          0.5-1 ms     ← ~5,000-10,000x ช้ากว่า RAM
Network (cross region):     100-200 ms   ← ระดับ "ล้านเท่า" (~1-2M x)
DB query (no index):        10-100 ms
DB query (cached):          1-5 ms

ผลกระทบของ cache hit vs miss:

text
Without cache:
  User → API → DB query → 50 ms
  
With cache (hit):
  User → API → Redis → 1 ms
  → 50x faster!

With cache (miss):
  User → API → Redis (miss) → DB → cache → 51 ms
  → ช้ากว่า no-cache เล็กน้อย แต่ครั้งถัดไป fast

กฎ 80/20 ของ cache

text
ในระบบจริง:
- 80% ของ request → 20% ของ data (**Pareto principle** อ่าน "พา-เร-โต" — economist Vilfredo Pareto ค้นพบว่า 80% ของผลมาจาก 20% ของต้นเหตุ)
- ถ้า cache 20% ที่ hot ได้ → hit rate ~80%

ตัวอย่าง: e-commerce
- Top 100 products = 80% ของ traffic
- Cache แค่ 100 records → save DB load มหาศาล

2. Cache Topologies — Cache อยู่ที่ไหน?

Topology 1: In-Process Cache (Local) — cache ใน process ตัวเอง

ตัวอย่าง:

  • Java: Caffeine (คาฟ-เฟอีน — high-performance, แนะนำสุดใน 2026), Ehcache (อี-เอช-แคช), Guava Cache (legacy)
  • Go: bigcache, freecache, ristretto (ริส-เตรท-โต — ตั้งชื่อตามกาแฟอิตาเลียน)
  • Node.js: node-cache, lru-cache

💡 รายการนี้ให้ "รู้ว่ามี" — ไม่ต้องจำทุกตัว เลือกตัวเดียวที่นิยมในภาษาของคุณ (Java → Caffeine, Go → ristretto, Node → lru-cache) ก็พอ

ข้อดี:

  • ✅ Latency ต่ำสุด (in-memory ของ process เดียวกัน)
  • ✅ ไม่มี network call
  • ✅ ตั้งค่าง่าย

ข้อเสีย:

  • ❌ ไม่ share — แต่ละ instance มี cache ของตัวเอง
  • ❌ Memory เปลือง (ทุก instance เก็บ copy เดียวกัน)
  • ❌ Inconsistency — instance A update DB, instance B's cache ยังเก่า
  • ❌ หาย เมื่อ restart (ไม่ persistent)

ใช้เมื่อ: Hot data ที่ไม่เปลี่ยนบ่อย, single-instance app, computation result

Topology 2: Distributed Cache (Shared) — cache กลางที่ทุก app เห็นเหมือนกัน

ตัวอย่าง:

  • Redis (most popular) — มี data structures + persistence + cluster; Redis 8.0 (2024) เพิ่ม multi-threaded I/O, Functions, vector search ผ่าน RediSearch built-in
  • Valkey (วาล-คีย์) — Linux Foundation fork ของ Redis (เม.ย. 2024) ใช้ license BSD; API-compatible 100%; AWS ElastiCache, GCP Memorystore รองรับแล้ว
  • Memcached — เบา + เร็ว + simple; Memcached 1.6+ (2020+) เพิ่ม extstore (SSD overflow), TLS, proxy — ยังนิยมใน 2026 สำหรับ ultra-high-throughput simple caching (Twitter/Etsy/Facebook scale)
  • Dragonfly (drag-on-fly, 2022+) — Redis API-compatible, multi-threaded architecture, ใช้ memory น้อยกว่า Redis ~3x
  • DiceDB (2024) — Redis-compatible, reactive (push updates), open source
  • Hazelcast — Java-native + computational grid
  • AWS ElastiCache — managed Redis / Valkey / Memcached

🚨 สำคัญ — Redis license change (มีนาคม 2024): Redis Inc. เปลี่ยน license จาก BSD → SSPL / RSALv2 (dual) → ไม่ใช่ open source แท้สำหรับ managed cloud providers อีกต่อไป → Linux Foundation fork เป็น Valkey (BSD license) → AWS / GCP / Oracle / Ericsson back Valkey

คำแนะนำสำหรับ deployment ใหม่ปี 2026:

  • ต้องการ open-source guarantee → Valkey (8.0 release พ.ย. 2024, API-compatible, drop-in replacement)
  • ใช้ Redis Stack / RedisInsight / commercial features → Redis Inc. (ภายใต้ license ใหม่)
  • cloud managed service → AWS ElastiCache รองรับทั้งคู่; Valkey ราคาถูกกว่า ~20%

ข้อดี:

  • ✅ Shared — ทุก instance เห็น cache เดียวกัน
  • ✅ Consistency ดีกว่า in-process
  • ✅ Memory efficient (เก็บ copy เดียว)
  • ✅ Survive app restart

ข้อเสีย:

  • ❌ Latency เพิ่ม (+0.5-1 ms network)
  • ❌ SPOF ถ้าไม่ replicate
  • ❌ Operate cluster เพิ่ม

ใช้เมื่อ: Multi-instance app, session storage, shared computation result

Topology 3: Multi-Tier Cache (cache หลายชั้น — Best of Both) — เอาดีจากทั้ง 2 แบบ

Flow:

text
Read request:
1. Check L1 (local) — hit? return (1 μs)
2. Check L2 (Redis) — hit? populate L1, return (1 ms)
3. Query DB — populate L2 + L1, return (50 ms)

Write:
1. Write DB
2. Invalidate L2
3. Invalidate L1 (broadcast to all instances)

Used ใน production scale — Facebook, Netflix, etc.

Topology 4: CDN (Edge Cache) — cache ใกล้ผู้ใช้ที่สุด

ใช้สำหรับ:

  • Static assets (CSS, JS, images, videos)
  • API response (read-heavy, geographic)
  • ปกป้อง origin จาก traffic

ตัวอย่าง: Cloudflare, AWS CloudFront, Fastly, Akamai


3. Caching Patterns — 4 Patterns หลัก

Pattern 1: Cache-Aside (Lazy Loading) — app เป็นคนจัดการ cache เอง (ที่นิยมที่สุด)

text
Read flow:
1. App: ขอ data → Cache
2. Cache hit: return ทันที
3. Cache miss: App → Query DB → populate Cache → return

Write flow:
1. App: write → DB
2. App: invalidate cache (delete key)

Code example:

typescript
async function getUser(userId: string) {
    // 1. Try cache
    const cached = await redis.get(`user:${userId}`);
    if (cached) {
        return JSON.parse(cached);
    }
    
    // 2. Miss → query DB
    const user = await db.findOne({ id: userId });
    
    // 3. Populate cache (with TTL)
    if (user) {
        await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 300);
    }
    
    return user;
}

async function updateUser(userId: string, data: any) {
    // 1. Update DB first
    await db.update({ id: userId }, data);
    
    // 2. Invalidate cache (let next read repopulate)
    await redis.del(`user:${userId}`);
}

ข้อดี:

  • ✅ Simple
  • ✅ Resilient — cache fail → app ยังทำงานได้ (degraded)
  • ✅ Only cache what's needed (lazy)

ข้อเสีย:

  • ❌ Cache miss latency = DB latency
  • ❌ Stale data risk if forgot to invalidate
  • ❌ Race condition (read miss + concurrent write)

Use case: ส่วนใหญ่ของ web app

Pattern 2: Read-Through — cache เป็นคนอ่านจาก DB ให้

text
App ขอ data → Cache Layer
Cache Layer:
  - hit: return
  - miss: query DB → populate self → return

App ไม่รู้จัก DB เลย — รู้แค่ Cache

Code example:

typescript
class CachedRepository {
    async get(id: string) {
        // Cache library handles miss automatically
        return await cache.getOrCompute(`user:${id}`, async () => {
            return await db.findOne({ id });
        }, { ttl: 300 });
    }
}

ข้อดี:

  • ✅ App code ไม่ซับซ้อน — cache layer จัดการ
  • ✅ Centralize caching logic

ข้อเสีย:

  • ❌ Cache layer ต้อง support DB access (เช่น CacheLoader ใน Caffeine)
  • ❌ Cold start ช้า (ครั้งแรก slow)

Use case: ORM-level caching, library-based caching

Pattern 3: Write-Through — เขียนผ่าน cache ก่อน DB

text
Write flow:
1. App: write → Cache
2. Cache: write → DB (synchronously)
3. Cache: return success ให้ app

Read flow:
1. App → Cache → return (always hit after first write)

ข้อดี:

  • ✅ Cache always in sync with DB (เฉพาะกรณีที่ implement เป็น atomic write — ดู caveat ด้านล่าง)
  • ✅ Read always fast (no miss after first write)

⚠️ Caveat — atomicity: "in sync" สมมติว่าการเขียน cache + DB เป็น atomic ของจริงไม่มี cross-system transaction → ถ้า cache write success แต่ DB fail (หรือกลับกัน) จะหลุดสมการ practice ที่นิยม: เขียน DB ก่อน success → ค่อยเขียน cache; ถ้า cache fail → invalidate (ลบ) แทน เพื่อให้ next read โหลด DB ใหม่

ข้อเสีย:

  • ❌ Write slow (2 hops)
  • ❌ Cache เปลือง memory (เก็บ data ที่อาจไม่ใช้)

Use case: Read-heavy + low write rate (e.g., product catalog)

Pattern 4: Write-Back (Write-Behind) — เขียน cache ก่อน เลื่อน DB ทีหลัง

text
Write flow:
1. App: write → Cache (fast!)
2. Cache: ACK ทันที (mark dirty)
3. Cache → DB (async, batched)

Risk: cache crash before flush → DATA LOSS!

ข้อดี:

  • ✅ Write latency ต่ำมาก (no DB wait)
  • ✅ Batched writes → high DB throughput

ข้อเสีย:

  • ❌ Data loss possible (cache crash before flush)
  • ❌ Complex
  • ❌ Cache must be durable (Redis with persistence)

Use case: High write rate + can tolerate data loss (analytics, metrics)

เปรียบเทียบ 4 Patterns

PatternReadWriteConsistencyใช้เมื่อ
Cache-AsideFast (after warmup)DB+Invalidateกลางส่วนใหญ่
Read-ThroughFast (lazy)App → DB → CacheกลางLibrary-based
Write-ThroughAlways fastSlow (Cache+DB)สูงRead-heavy
Write-BackFastVery fastต่ำ (eventual)High write, tolerate loss

4. Cache Invalidation — "หนึ่งใน 2 hard things"

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

4 Strategies หลัก

Strategy 1: TTL (Time-To-Live) — ตั้งวันหมดอายุ

text
"Cache expires after X seconds"

✅ Simple — ตั้ง TTL = 5 นาที, ปล่อยให้หมดอายุเอง
❌ Stale data ระหว่าง update → TTL หมด
❌ Cache miss spike เมื่อ TTL หมดพร้อมกัน

ตัวอย่าง TTL ตาม use case:

Data typeTTL ที่เหมาะ
Static config1 hour - 1 day
User profile5-30 min
Product price1-5 min
Stock market data1-5 sec
Real-time chatไม่ cache

Strategy 2: Explicit Invalidation — ลบ cache ทันทีที่ update

text
Write → DB → DELETE cache key

✅ Cache ตรงกับ DB เสมอ (ทันที)
❌ ต้องจำว่า key ไหนเกี่ยวข้องกับ data ไหน
❌ Cross-service invalidation ยาก

ตัวอย่าง (E-commerce):

typescript
async function updateProductPrice(productId, newPrice) {
    await db.update({ id: productId }, { price: newPrice });
    
    // Invalidate ทุก key ที่เกี่ยวข้อง
    await redis.del(`product:${productId}`);
    await redis.del(`product:list:popular`);
    await redis.del(`category:${product.categoryId}:products`);
    
    // ถ้าโชว์ในหน้า search → invalidate search cache
    await invalidateSearchCache(productId);
}

Common pitfall: ลืม invalidate บาง key → stale data!

Strategy 3: Versioning / Cache Tags — ใช้ version แทนการลบ

แทนที่จะ delete — เพิ่ม version หรือ tag

text
Cache key with version:
  cache_key = `product:${productId}:v${product.version}`
  
On update:
  product.version++ (atomic)
  
On read:
  Get product.version → build cache key → check cache
  → ถ้า version เปลี่ยน, key ใหม่ = miss → repopulate
  → ค่าเก่ายังอยู่ใน cache แต่ไม่มีใครเข้าถึง

ข้อดี:

  • ✅ ไม่มี race condition
  • ✅ Old cache evicts ตาม LRU เอง

ข้อเสีย:

  • ❌ Memory เปลือง (เก็บ version เก่า)

Strategy 4: Event-driven Invalidation — แจ้งผ่าน event ให้ทุก service

text
Service A: update DB → publish event "ProductUpdated" → Kafka
Service B: subscribe → delete cache key
Service C: subscribe → delete cache key

ใช้กับ multi-service architecture ที่ cache อยู่หลายที่

typescript
// Service A
async function updatePrice(productId, price) {
    await db.update({ id: productId }, { price });
    await kafka.publish('product-events', {
        type: 'PriceChanged',
        productId,
    });
}

// Service B (any service that caches product)
kafka.subscribe('product-events', async (event) => {
    if (event.type === 'PriceChanged') {
        await redis.del(`product:${event.productId}`);
    }
});

5. Cache Stampede — ปัญหาที่ทุกคนต้องเจอ

ปัญหา

text
Popular cache key หมดอายุที่เวลา 12:00:00 พอดี
ในวินาทีนั้น มี 10,000 concurrent requests:
- ทั้ง 10,000 เห็น cache miss
- ทั้ง 10,000 query DB พร้อมกัน!
- DB overload → ทุก request slow
- Cache populate → finally
- DB recover

ผลกระทบ: 1-2 วินาทีของ latency spike + อาจ DB ล่ม

Solution 1: Probabilistic Early Expiration (XFetch) — ลุ้น refresh ก่อนหมดอายุ

text
แทนที่จะรอ TTL หมด → "ลุ้น" expire ก่อน TTL จริง

อัลกอริทึม:
- TTL = 300 sec
- ตอนเหลือ < 30 sec (10%): มี chance เล็กน้อยที่จะ refresh
- เมื่อใกล้หมด: chance เพิ่มขึ้น
- ส่งผลให้บาง request "อาสา" refresh ก่อนหมด

ผลลัพธ์: cache ไม่มีวัน "ตาย" พร้อมกัน — recompute spread ในช่วงเวลา

Solution 2: Single-Flight (Lock-based) — มีคนเดียวที่ recompute

text
Cache miss:
1. ลองได้ "lock" สำหรับการ recompute
2. ได้ lock → query DB + populate cache + release lock
3. ไม่ได้ lock → wait → re-check cache (จะ hit ในไม่ช้า)

ผลลัพธ์: only 1 query DB ต่อ key ต่อ time

Code example:

typescript
const inFlight = new Map<string, Promise<any>>();

async function getCachedWithSingleFlight<T>(
    key: string,
    fetcher: () => Promise<T>,
    ttl: number
): Promise<T> {
    // 1. Try cache
    const cached = await redis.get(key);
    if (cached) return JSON.parse(cached);
    
    // 2. Check if there's already a fetch in flight
    if (inFlight.has(key)) {
        return await inFlight.get(key);
    }
    
    // 3. Start fetch + register as in-flight
    const promise = (async () => {
        try {
            const value = await fetcher();
            await redis.set(key, JSON.stringify(value), 'EX', ttl);
            return value;
        } finally {
            inFlight.delete(key);
        }
    })();
    
    inFlight.set(key, promise);
    return await promise;
}

→ Go มี singleflight package ในตัว

Solution 3: Stale-While-Revalidate (SWR) — เสิร์ฟค่าเก่าระหว่าง refresh

text
Cache returns stale value while async background refresh

Flow:
- Cache hit (fresh) → return
- Cache hit (stale, but not too stale) → return + trigger refresh async
- Cache miss → query DB + return

ผลลัพธ์: User ไม่เคยรอ DB query
        Stale ได้ ~few seconds — แต่ระบบไม่ล่ม

Best practice สำหรับ read-heavy (Cloudflare ใช้แบบนี้)

Solution 4: Cache Warming — อุ่น cache ไว้ก่อนหมด

text
ก่อน TTL หมด → background job refresh

Cron job:
  ทุก 4 นาที (สำหรับ TTL 5 นาที):
    - Fetch top 100 hot products
    - Update cache
  
→ Cache แทบไม่เคย miss

6. Redis Cluster — scale-out cache แบบ shard

Single Redis Limitations — ข้อจำกัดของ Redis ตัวเดียว

text
Single Redis:
- Memory limit: 1 server's RAM (~128 GB max)
- Throughput: ~100K-1M ops/sec
- SPOF: server ตาย = cache หาย

ต้องการมากกว่านี้:
→ Redis Cluster (sharding)
→ Redis Sentinel (HA)

Redis Cluster Architecture — สถาปัตยกรรม Redis Cluster

Key → CRC16(key) % 16384 → slot → node

Client behavior:

text
Client: SET user:42 "Anna"

1. Compute: CRC16("user:42") % 16384 = slot 5234
2. Slot 5234 belongs to Node B
3. Send to Node B directly

ถ้า client ส่งไป Node ผิด:
- Node ตอบ MOVED redirect
- Client cache slot map + redirect

Hash Tags — บังคับให้ key อยู่ slot เดียวกัน

text
Problem: WANT to do MULTI ops on related keys
   MGET user:42 user:43 user:44
   → keys อาจอยู่คนละ node → ไม่ work

Solution: use hash tags { ... }
   MGET {user}:42 {user}:43 {user}:44
   → ใช้แค่ "user" คำนวณ hash → ทุก key ไป slot เดียวกัน

ข้อดี / ข้อเสีย

text
✅ Scale horizontally (add nodes → more memory + throughput)
✅ Auto resharding
✅ Replicas สำหรับ HA

❌ MULTI/Transactions limited to single slot
❌ Cross-slot operations require hash tags
❌ Complexity (operate cluster vs single)

7. Cache Eviction Policies — Memory เต็มแล้วทำยังไง?

เมื่อ cache memory เต็ม — ต้อง "kick out" บาง entries

นโยบายที่นิยมใช้

Policyกลไกใช้เมื่อ
LRU (Least Recently Used)ลบที่ "อ่านล่าสุด" นานที่สุดWorkload ที่ recent = hot (web cache)
LFU (Least Frequently Used)ลบที่ "เข้าถึงน้อยที่สุด"Long-tail access pattern
FIFOลบที่ใส่ก่อนSimple, แต่ไม่ค่อยฉลาด
Randomสุ่มลบเมื่อ analyze pattern ไม่ได้
TTL-basedลบที่ใกล้ expireTTL-heavy workload

Redis Eviction Policies — นโยบายของ Redis

text
maxmemory-policy:
  noeviction       — Reject writes ถ้าเต็ม (default)
  allkeys-lru      — LRU จากทุก key
  allkeys-lfu      — LFU จากทุก key
  allkeys-random   — Random
  volatile-lru     — LRU จาก keys with TTL
  volatile-lfu     — LFU จาก keys with TTL
  volatile-random  — Random จาก keys with TTL
  volatile-ttl    — ลบที่ TTL ใกล้หมด

ตัวเลือกที่นิยม:

text
allkeys-lru     — web app cache (recent = hot)
volatile-lru    — mixed: cache + persistent data
allkeys-lfu     — popularity-driven (e.g., trending)

8. Cache Sizing — เก็บกี่ MB ดี?

Working Set Analysis — วิเคราะห์ data ที่ใช้บ่อย

text
ขั้นตอน:
1. วัด query pattern (DB query log)
2. หา "working set" = data ที่ถูกเข้าถึงใน last X (เช่น last 1 hour)
3. Cache size = working set + 20% headroom

กฎรวมๆ

text
Hit rate target:
- 80%+ — ดี
- 90%+ — ดีมาก
- 95%+ — excellent

ถ้า hit rate < 70% → cache ของคุณ "underutilized"
- Cache size เล็กเกินไป → เพิ่ม memory
- หรือ TTL สั้นเกินไป → เพิ่ม TTL
- หรือ workload ไม่เหมาะ cache (random access)

Memory Calculation — คำนวณ memory ที่ต้องใช้จริง

text
Average entry: 1 KB (user record)
Working set: 1M entries
Required cache: 1 GB + 20% = 1.2 GB

แต่ Redis overhead:
- Key (string): ~50-100 bytes overhead
- Value serialization (JSON): 2-3x size
- Hash table: ~30% overhead

จริงๆ ใช้ ~2-3 GB สำหรับ 1M entries (1 KB each)

9. Cache + Database Consistency — ทำให้ cache กับ DB ตรงกัน

ปัญหาคลาสสิกของ cache คือมันอาจไม่ตรงกับ DB — โดยเฉพาะเมื่อมี race condition (thread หนึ่งอ่านค่าเก่ามา populate cache หลังอีก thread อัปเดต DB ไปแล้ว) ทำให้ cache ค้างค่าเก่า ส่วนนี้สอนวิธีรับมือ: version check (CAS), การลบ cache ให้ถูกจังหวะ และ negative caching:

ปัญหา — Race Condition

text
Time | Thread A                | Thread B
0    | Read DB (value = 5)    |
1    |                         | Write DB (value = 10)
2    |                         | Invalidate cache
3    | Populate cache (5)     |
4    | ← cache has 5          |
5    | DB has 10              |
6    | Cache stale ❌

Solution: Lock or version check

typescript
async function updateUser(userId, data) {
    // 1. Update DB with version increment
    const updated = await db.update(
        { id: userId, version: data.version },  // CAS
        { ...data, version: data.version + 1 }
    );
    
    if (!updated) throw new ConflictError();
    
    // 2. Delete cache
    await redis.del(`user:${userId}`);
    
    // 3. Optionally — populate cache with new value (write-through)
    // await redis.set(`user:${userId}`, JSON.stringify(updated), 'EX', 300);
}

Negative Caching — Cache "ไม่พบ"

text
ปัญหา: 
  Attack pattern — query user:999999 (ไม่มี) ซ้ำๆ
  → DB query every time (slow)
  → DoS attack

Solution: cache the "not found" result
  - user:999999 → cache "NULL" (with short TTL, 30s)
  - subsequent reads → return NULL ทันที (no DB hit)

⚠️ ระวัง: ถ้า user ถูกสร้างจริง — invalidate ทันที

10. CDN — Cache at Edge — cache ที่ขอบเครือข่าย

CDN คืออะไร

text
CDN = "Content Delivery Network"
- เครือข่าย server กระจายทั่วโลก
- Cache static content (และบาง API)
- Serve user จาก server ใกล้ที่สุด

User ใน Bangkok:
  → Cloudflare PoP Bangkok (5 ms)
  
User ใน NYC:
  → Cloudflare PoP NYC (5 ms)

(แทนที่จะวิ่งไป origin ใน US-East = 200+ ms)

CDN Caching Layers — ชั้นการ cache ของ CDN

text
1. Browser cache (client)
   - Cache-Control: max-age=3600
   - User's local

2. CDN edge cache
   - Closest PoP

3. CDN shield (mid-tier)
   - Regional cache (less PoPs but cached)

4. Origin
   - Your servers

Cache-Control Headers — header ควบคุม cache

http
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=600

Breakdown:

text
public           — Anyone can cache (CDN, browser)
private          — Only browser, not CDN
no-cache         — Must revalidate every time
no-store         — Don't cache at all (sensitive data)
max-age=3600     — Browser cache 1 hour
s-maxage=86400   — Shared (CDN) cache 24 hours
stale-while-revalidate=600  — Serve stale 10 min while refreshing

CDN for Dynamic Content — ใช้ CDN กับ content แบบ dynamic

text
Traditional: CDN cache only static (CSS, JS, images)
Modern: cache "edge logic" — Cloudflare Workers, Lambda@Edge

Use cases:
✅ API response caching (with cache key by user/region)
✅ A/B testing
✅ Authentication at edge
✅ Image resizing
✅ Personalization

11. ดูตัวอย่างจริง — Cache Architecture ของ Instagram

มาดูว่าระบบจริงระดับ Instagram ใช้ cache หลายชั้นยังไง — CDN สำหรับ static, Memcached สำหรับ hot data, Redis สำหรับ counter/feed และ Cassandra เป็น truth ผลคือ Cassandra รับ read แค่ ~5% ที่เหลือถูก cache ดูดไว้ ตัวอย่างนี้ตอบว่า "ทำไมต้องมี cache หลายชั้น" — เพราะ workload แต่ละแบบเหมาะกับเครื่องมือต่างกัน:

Layers:

  • CDN: static assets + photo CDN
  • Memcached: hot user/post data (cache-aside)
  • Redis: counters (likes, followers), sorted sets (feeds)
  • Cassandra: persistent storage (truth)

Hit rate:

  • CDN: 95%+
  • Memcached: 85%+
  • → Cassandra serves only ~5% of read traffic

ℹ️ Disclaimer: ตัวอย่างนี้เป็น illustrative pattern ของ multi-tier cache ไม่ใช่ architecture จริงปัจจุบันของ Instagram — Instagram จริงใช้ Cassandra + Memcached แต่ก็ใช้ TAO (Facebook social graph cache) และ system proprietary อื่น ๆ ที่ไม่ public รายละเอียด ใช้ตัวอย่างนี้เพื่อเข้าใจ "ทำไม cache หลายชั้น" — ไม่ใช่ blueprint

Why multiple cache layers? เพราะ workload ต่างกัน:

  • Static → CDN (geo)
  • Hot reads → Memcached (simple key-value)
  • Counters → Redis (atomic INCR)

12. Common Pitfalls — ข้อพึงระวังที่เจอบ่อย

❌ Pitfall 1: ใช้ cache เป็น primary store

text
Bad: 
  write → cache only (no DB)
  → cache crash = lose data

Good: 
  write → DB (truth) → cache (copy)

❌ Pitfall 2: Cache stampede ไม่ป้องกัน

text
Bad: TTL หมดพร้อมกัน 10,000 keys → DB hit 10,000 ครั้ง

Good: stale-while-revalidate + single-flight

❌ Pitfall 3: ลืม invalidate

text
Bad:
  updateUser(42, {name: "Anna"}) 
  → DB updated, cache user:42 still old

Good: list ทุก cache key + invalidate ทุกที่
       หรือใช้ event-driven invalidation

❌ Pitfall 4 — Cache ทุกอย่าง (anti-pattern)

text
Bad: cache ทุก query, ทุก request → memory เต็ม + low hit rate

Good: cache เฉพาะ "expensive + frequent" queries
      (computed via DB query log analysis)

❌ Pitfall 5: ไม่ monitor hit rate

text
ต้อง track:
- hit rate (target > 80%)
- evictions/sec (สูงเกินไป = memory ไม่พอ)
- latency p99 (สูงเกินไป = cache slow)
- memory usage

13. Cache Anti-Patterns — รูปแบบที่ควรหลีกเลี่ยง

ปิดท้ายด้วยรูปแบบการใช้ cache ที่ควรเลี่ยง — cache ข้อมูลที่เปลี่ยนบ่อย (hit rate ต่ำ), cache ข้อมูลลับ (เสี่ยง leak), cache โดยไม่มี TTL (memory leak) รู้กับดักเหล่านี้ไว้จะช่วยให้ใช้ cache ได้คุ้มจริงแทนที่จะกลายเป็นภาระ:

text
1. Caching frequently-changing data
   - ถ้า data เปลี่ยนทุก request → cache มี hit rate ต่ำ → ทำลายประสิทธิภาพ

2. Caching sensitive data (passwords, tokens)
   - Cache อาจ leak (logs, backups)
   - Always encrypt + short TTL

3. Caching with no TTL
   - Memory leak → server crash

4. Same cache for multi-tenant
   - User A เห็น data ของ User B (key collision)
   - Always include tenant_id ใน cache key

5. Cache key collision
   - "user:42" — user 42 ใน app A?  หรือ B?
   - Always prefix: "app1:user:42"

14. Checkpoint — แบบฝึกหัด

🛠️ Checkpoint 8.1 — Cache-Aside Lab ตั้ง Spring Boot + Redis

  • implement cache-aside สำหรับ User entity
  • ทดสอบ hit/miss/invalidate
  • วัดว่า latency ดีขึ้นแค่ไหน

🛠️ Checkpoint 8.2 — Cache Stampede จำลอง:

  • 1000 concurrent request สำหรับ key เดียวกัน
  • ตอน TTL หมด → ดู DB load
  • implement single-flight → ทดสอบใหม่ → ยืนยันว่า DB ถูก hit แค่ 1 ครั้ง

🛠️ Checkpoint 8.3 — Multi-Tier Cache สร้าง cache 2 ชั้น local (Caffeine) + Redis

  • วัด L1 hit rate, L2 hit rate, miss rate
  • เทียบ latency กับแบบชั้นเดียว

🛠️ Checkpoint 8.4 — Redis Cluster ตั้ง Redis cluster 3 shard (Docker)

  • ใส่ 10K key → ดูการกระจาย
  • kill 1 node → ยืนยัน failover
  • ลอง MGET ข้าม slot → ต้องใช้ hash tags

🛠️ Checkpoint 8.5 — Cache Headers + CDN deploy static site หลัง Cloudflare

  • ตั้ง Cache-Control header
  • วัด CDN hit rate
  • ลอง purge จาก dashboard

15. สรุปบท

Cache = "เก็บไว้ใกล้ ๆ" — reduce latency + DB load ✅ Topologies: In-process, Distributed (Redis), Multi-tier, CDN4 Patterns: Cache-Aside (default), Read-Through, Write-Through, Write-Back ✅ Invalidation: TTL, Explicit, Versioning, Event-driven — แต่ละแบบมี trade-off ✅ Cache Stampede: ใช้ single-flight + stale-while-revalidate ป้องกัน ✅ Redis Cluster = sharding 16384 slots, hash tags สำหรับ multi-key ✅ Eviction: LRU เป็นค่าปกติ, LFU สำหรับ popularity workload ✅ CDN: cache at edge — Cache-Control + s-maxage + stale-while-revalidate ✅ Anti-patterns: cache as primary store, no TTL, no monitoring, key collision


← บทที่ 7 | บทที่ 9 → Failure Detection + DR