🛑 1. The Problem First: "ข้อมูลผีหลอก" (Stale Data & DB Crash)
ลองนึกถึงแอปที่มีคนเข้าเยอะๆ แล้วคุณพยายามจะลดภาระ Database โดยการใส่ Cache:
// ❌ Naive Approach: ใส่ Cache แบบไม่มีวันตาย (No TTL)
const user = await redis.get("user:1");
if (!user) {
const data = await db.user.find(1);
await redis.set("user:1", data); // 🌋 ระเบิดเวลา! ถ้า User เปลี่ยนชื่อ
// Cache เดิมจะยังคงชื่อเก่าไว้ตลอดกาล จนกว่า Server จะรีสตาร์ท!
}
ปัญหา: การใส่ Cache โดยไม่มีกลยุทธ์การลบ (Invalidation) จะทำให้เกิด "ข้อมูลไม่ตรงกัน" (Inconsistency) ซึ่งเป็นฝันร้ายของงานพาณิชย์ และที่แย่กว่านั้นคือถ้า Cache หมดอายุพร้อมกันหมื่นคน (Thundering Herd) Database ของคุณที่ไม่ได้เตรียมรับแรงกระแทกจะล่มสลายทันที นี่คือสาเหตุที่ Senior ต้องมองการ Cache เป็นเรื่องของ "การจัดการเวลา" ครับ
💡 2. Real-Life Analogy: สมุดจดออเดอร์หน้าเคาน์เตอร์
- Cache-Aside (Lazy Loading): เหมือน "คนรับออเดอร์ที่จดเมนูโปรดไว้ในหัว". ถ้าลูกค้าสั่งเมนูที่มีในหัว (Hit) เขาก็ทำได้เลย ถ้าไม่มี (Miss) เขาต้องเดินไปดูตำราหลังบ้าน (Database) แล้วจดลงในกระดาษโน้ตแปะไว้ดูครั้งหน้า
- Write-Through: เหมือนการ "จดออเดอร์ลงสมุดและส่งเข้าครัวพร้อมกัน". ข้อมูลในมือคนรับกับในครัวจะตรงกันเสมอ แต่พนักงานจะทำงานช้าลงนิดนึงเพราะต้องเขียนสองรอบ
- TTL (Time to Live): เหมือน "อาหารที่ต้องมีวันหมดอายุ". แม้จะยังขายไม่หมด แต่ถ้าเกินเวลาที่กำหนด เราต้องทิ้ง (ลบ Cache) เพื่อความปลอดภัยของลูกค้า (ความถูกต้องของข้อมูล)
🚀 3. Execution Journey: ขั้นตอนการจำแบบเซียน (Cache-Aside)
เราจะใช้ Redis เป็นหน่วยความจำเสริมเพื่อช่วยลดภาระ Database
🛠 Step-by-step:
- The Request: แอปมองหาข้อมูลใน Redis (ความเร็วระดับ Millisecond)
- The Decision:
- HIT: คืนข้อมูลทันที (จบงาน!)
- MISS: เดินไปดึงข้อมูลจาก Database ที่ช้ากว่า 100 เท่า
- The Populate: นำข้อมูลใหม่ที่ได้มาใส่ใน Redis พร้อมตั้งเวลาตาย (TTL)
- The Buffer: เพิ่มค่า "Jitter" สุ่มเวลาหมดอายุ (เช่น 5 นาที +/- 10 วินาที) เพื่อไม่ให้ Cache ทุุกตัวตายพร้อมกัน
// ✅ Best Practice: กลยุทธ์ Cache-Aside พร้อมความปลอดภัย
async function getProduct(id: string) {
const cacheKey = `product:${id}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached); // ⚡ เร็วแรง!
const product = await db.product.find(id);
if (product) {
// 🛠 ตั้งเวลาตาย 5 นาที + สุ่มเพิ่มลดเล็กน้อย (Prevent Thundering Herd)
await redis.set(
cacheKey,
JSON.stringify(product),
"EX",
300 + Math.random() * 10,
);
}
return product;
}
🪤 4. The Junior Trap: โรค "Cache ทุกอย่างที่ขวางหน้า"
จูเนียร์มักจะใส่ Cache ให้กับข้อมูลที่เปลี่ยนสีได้ตลอดเวลา:
// ❌ Junior Trap: ใส่ Cache ให้กับยอดเงินคงเหลือ (User Balance)
await redis.set(`balance:${userId}`, 100);
// 🌋 พัง! ถ้ายอดเงินจริงถูกหักไปแล้ว แต่ Cache ยังบอกว่ามี 100
// User อาจจะกดถอนเงินได้เกินจำนวนจริง (Double Spending)
ระวัง: อย่า Cache ข้อมูลที่ "ความผิดพลาดมีราคาแพง" ✅ การแก้ไข: ข้อมูลด้านการเงินหรือสถานะที่ต้อง Real-time 100% ควรดึงจาก Database ที่เป็น "Single Source of Truth" เท่านั้น
⚖️ 5. The Why Matrix: เลือกกลยุทธ์ให้ถูกงาน
| กลยุทธ์ | ข้อดี (Pro) | ข้อด้อย (Con) | เหมาะกับ |
|---|---|---|---|
| Cache-Aside | ⚡ ประหยัดทรัพยากร | Request แรกช้า (Cold Start) | ข้อมูลทั่วไป (Catalog) |
| Write-Through | ⚡⚡ ข้อมูลแม่นยำเสมอ | เขียนข้อมูลช้าลง | ข้อมูลสำคัญที่อ่านบ่อย |
| TTL Only | ง่ายที่สุด ไม่ต้องคุมเยอะ | ข้อมูลอาจเก่าชั่วคราว | ข้อมูลที่ไม่ซีเรียสมาก |
| Manual Invalidate | 🚀 แม่นยำที่สุด | เขียนโค้ดยาก (ซับซ้อน) | ระบบต้องการความเป๊ะสูง |
🎓 6. Senior Mindset Summary
การเป็น Senior คือการมองว่า "Cache ไม่ใช่คำตอบของทุุกปัญหา Performance". ก่อนจะใส่ Cache จงลอง Optimize Database ของคุณให้ดีที่สุดก่อน เพราะ Cache ที่ดีที่สุดคือ Cache ที่ 'ไม่มีอยู่จริง' แต่ระบบยังทำงานได้รวดเร็วด้วยตัวเองครับ!