ข้ามไปยังเนื้อหา

Caching (Cache-aside)

pattern cache-aside (หรือที่รู้จักในชื่อ lazy loading) ให้แอปพลิเคชันของคุณควบคุมว่าอะไรจะเข้าไปใน cache และเมื่อไหร่ Redis อยู่หน้าฐานข้อมูลของคุณ: ถ้าข้อมูลอยู่ที่นั่น ก็ข้ามฐานข้อมูลไปได้เลย ถ้าไม่มี ก็ดึงจาก DB แล้วนำมาเก็บใน cache สำหรับครั้งถัดไป

flowchart TD
  A["App needs cache:user:42"] --> B["GET cache:user:42"]
  B --> C{"Cache hit?"}
  C -->|"Yes"| D["Return cached value"]
  C -->|"No (nil)"| E["Query DB"]
  E --> F["SET cache:user:42 result EX 300"]
  F --> G["Return value"]
Cache-aside read flow: check Redis, fall back to the DB on a miss, then write back with a TTL

แอปพลิเคชันเป็นผู้ควบคุมทั้งการอ่านและการเขียนลงใน cache ไม่มีการโหลดข้อมูลล่วงหน้า รายการถูกสร้างขึ้นตามความต้องการและหมดอายุโดยอัตโนมัติผ่าน TTL

127.0.0.1:6379> GET cache:user:42
(nil)
127.0.0.1:6379> SET cache:user:42 "{\"name\":\"Ada\",\"email\":\"[email protected]\"}" EX 300
OK
127.0.0.1:6379> TTL cache:user:42
(integer) 299
127.0.0.1:6379> GET cache:user:42
"{\"name\":\"Ada\",\"email\":\"[email protected]\"}"

EX 300 กำหนด TTL เป็น 300 วินาที (5 นาที) หลังจากนั้น key จะหายไปและคำขอถัดไปจะเป็น cache miss ซึ่งจะทำให้อ่านข้อมูลใหม่จาก DB

GET cache:user:42
SET cache:user:42 "{\"name\":\"Ada\",\"email\":\"[email protected]\"}" EX 300
TTL cache:user:42
GET cache:user:42

เมื่อข้อมูลต้นฉบับเปลี่ยนแปลง เช่น ผู้ใช้อัปเดตโปรไฟล์ของตน รายการใน cache จะกลายเป็นข้อมูลเก่า วิธีที่ง่ายและเชื่อถือได้ที่สุดคือการลบ key ทันทีเพื่อให้การอ่านครั้งถัดไปดึงข้อมูลใหม่จาก DB

127.0.0.1:6379> SET cache:user:42 "{\"name\":\"Ada updated\"}" EX 300
OK
127.0.0.1:6379> DEL cache:user:42
(integer) 1
127.0.0.1:6379> GET cache:user:42
(nil)
SET cache:user:42 "{\"name\":\"Ada updated\"}" EX 300
DEL cache:user:42
GET cache:user:42

cache stampede เกิดขึ้นเมื่อ key ยอดนิยมหมดอายุและคำขอพร้อมกันหลายสิบ (หรือหลายพัน) รายการเกิด cache miss ในเวลาเดียวกัน ทั้งหมดแข่งกันไปยังฐานข้อมูลพร้อมกัน ทำให้เกิดการพุ่งสูงของภาระบน DB อย่างกะทันหัน — นั่นคือสิ่งที่ caching ตั้งใจป้องกันอยู่

มีสองวิธีที่ใช้บรรเทาปัญหานี้ทั่วไป: ใช้ SET ... NX เพื่อให้เฉพาะผู้เขียนรายแรกเท่านั้นที่นำข้อมูลเข้า key และให้รายอื่นรอ หรือใช้ probabilistic early expiration — เทคนิคที่รีเฟรช key แบบสุ่มก่อนที่จะหมดอายุ โดยอ้างอิงจาก TTL ที่เหลืออยู่และต้นทุนการคำนวณที่ประมาณไว้ ทั้งสองกลยุทธ์ช่วยลดผลกระทบ thundering-herd โดยไม่ต้องใช้บริการ lock ภายนอก

ตัวเลือกBenefitCost
Cache-asideแอปควบคุมเองว่าอะไรเข้า cache; DB ไม่ถูกโหลดถ้าไม่มีใครขอข้อมูลนั้นcache miss ครั้งแรกช้ากว่าปกติ (query DB + write cache)
Write-throughcache กับ DB sync กันเสมอ ไม่มี stale readเขียนทุกครั้งช้าลง เพราะต้องเขียนทั้งสองที่ก่อน return
Probabilistic early expirationลด thundering herd ตอน key ยอดนิยมหมดอายุต้องคำนวณ probability ต่อ request เพิ่ม logic ในแอป
  • ไม่ตั้ง TTL ให้ cache key — ถ้าลืมใส่ EX ข้อมูลจะค้างอยู่ตลอดไปแม้ source-of-truth เปลี่ยนไปแล้ว กลายเป็น stale cache ถาวร
  • ปล่อยให้เกิด cache stampede โดยไม่มีการป้องกัน — เมื่อ key ยอดนิยมหมดอายุพร้อมกัน คำขอนับพันจะพุ่งไป DB พร้อมกัน ควรใช้ SET ... NX หรือ probabilistic early expiration
  • ลืม invalidate cache ตอนข้อมูลต้นฉบับเปลี่ยน — ถ้าอัปเดต DB แต่ไม่ DEL cache key เดิม ผู้ใช้จะเห็นข้อมูลเก่าไปจนกว่า TTL จะหมด

💡 ตัวอย่างจากของจริง

Twitter — ใช้ cache-aside pattern แบบเดียวกับบทนี้เป็นแกนของ timeline cache โดยเก็บ tweet ล่าสุดไว้ใน Redis แล้ว fallback ไป storage ชั้นในเมื่อ miss

Stack Overflow — วาง Redis cache-aside ไว้หน้า SQL Server เพื่อลด query ซ้ำสำหรับหน้าที่มีคนอ่านพร้อมกันจำนวนมาก

ใน cache-aside แอปพลิเคชันเขียนลง Redis เมื่อไหร่?
คำสั่งใดที่ลบ cache entry ที่ล้าสมัย?
cache stampede คืออะไร?
option ใดของ SET ที่กำหนด TTL เป็นวินาที?