โหมดมืด
บทที่ 8 — Distributed Caching
หลังจบบท คุณจะ:
- เข้าใจว่าทำไม cache สำคัญ + ทำงานยังไง
- เลือก caching pattern ที่เหมาะกับ workload — 4 patterns หลักสรุปสั้น ๆ ก่อน:
| Pattern | ใครเขียน cache? | ใครอ่าน DB ตอน miss? |
|---|---|---|
| Cache-aside (lazy loading) | app จัดการเอง | app |
| Read-through | cache library จัดการ | cache (ผ่าน loader) |
| Write-through | app เขียน 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 เลย — รู้แค่ CacheCode 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
| Pattern | Read | Write | Consistency | ใช้เมื่อ |
|---|---|---|---|---|
| Cache-Aside | Fast (after warmup) | DB+Invalidate | กลาง | ส่วนใหญ่ |
| Read-Through | Fast (lazy) | App → DB → Cache | กลาง | Library-based |
| Write-Through | Always fast | Slow (Cache+DB) | สูง | Read-heavy |
| Write-Back | Fast | Very 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 type | TTL ที่เหมาะ |
|---|---|
| Static config | 1 hour - 1 day |
| User profile | 5-30 min |
| Product price | 1-5 min |
| Stock market data | 1-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 ต่อ timeCode 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 แทบไม่เคย miss6. 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 + redirectHash 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 | ลบที่ใกล้ expire | TTL-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 serversCache-Control Headers — header ควบคุม cache
http
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=600Breakdown:
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 refreshingCDN 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
✅ Personalization11. ดูตัวอย่างจริง — 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 usage13. 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, CDN ✅ 4 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