Thundering Herd Problem: เมื่อ Cache หมดอายุวินาทีเดียว ทำไม Database ถึงโดนฝูงควายป่าเหยียบตาย?
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 15 วินาที
เที่ยงคืนเป๊ะ... แคมเปญ Flash Sale 9.9 เริ่มต้นขึ้น มีผู้ใช้งาน 50,000 คนพร้อมใจกันกดรีเฟรชหน้าสินค้าชิ้นเดียวกัน
คุณตั้งค่า Redis Cache ไว้รองรับอย่างดี ข้อมูลถูกยิงตอบกลับอย่างรวดเร็วระดับ 1ms ระบบนิ่งสนิท... แต่พอผ่านไป 1 ชั่วโมง ทันทีที่เข็มนาฬิกาตี 01:00 น. ตรง Database หลักกลับ CPU พุ่งทะลุ 100% แล้วน็อกไปเฉยๆ ทั้งที่จำนวนคนเข้าชมไม่ได้เพิ่มขึ้นเลยสักคนเดียว!
🐃 ฝูงควายป่าตกใจเสียงฟ้าร้อง (Cache Stampede)
สาเหตุไม่ได้มาจากบั๊กในโค้ด แต่มาจากพฤติกรรมพื้นฐานของแพทเทิร์น Cache-Aside:
if (data == null) {
data = database.query(key) // ดึงจาก DB
redis.set(key, data, ttl = 3600) // แคชไว้ 1 ชม.
}
return data
คุณตั้ง TTL ไว้ 3,600 วินาที (1 ชั่วโมง) หมายความว่าในวินาทีที่ 3,601 แคชตัวนี้จะระเหยหายไปจาก Redis พร้อมกันทันที
และในเสี้ยววินาทีนั้นเอง ที่มีคนขอดูสินค้านี้อยู่พร้อมกัน 5,000 คน... ทั้ง 5,000 Threads หันไปมองที่ Redis แล้วพบว่า "อ้าว ไม่มีของนี่หว่า!"
"เปรียบเหมือนฝูงควายป่า (5,000 Requests) ที่วิ่งมาเจอประตูรั้วเปิดอยู่ เลยพร้อมใจกันกรูทะลักเข้าไปเหยียบร้านขายของชำเล็กๆ (Database) พร้อมกันจนราบเป็นหน้ากลอง"
🛡️ 2 คาถาปราบฝูงควาย: Mutex Lock & Random Jitter
1. แจกกุญแจคนเดียว (Distributed Lock / Mutex)
แทนที่จะปล่อยให้ทุกคนวิ่งลงไปแย่งกันคิวรี่ DB ใครที่พบว่าแคชว่าง
ต้องไปหยิบ Lock จาก Redis ก่อน (เช่นใช้คำสั่ง SET NX)
- คนที่ได้กุญแจคนแรก ➔ วิ่งไปดึงข้อมูลจาก Database มาเขียนลง Cache
- อีก 4,999 คนที่เหลือกดแย่งกุญแจไม่ทัน ➔ ให้หลับรอ (Sleep) สัก 50ms แล้วค่อยหันกลับมาอ่านจาก Cache ใหม่อีกรอบ
2. ใส่ความมั่วให้เวลาหมดอายุ (TTL Jitter)
ถ้าคุณมีสินค้า 10,000 รายการ อย่าตั้ง TTL 3,600 วินาทีเป๊ะทุกตัว! เพราะมันจะพากันหมดอายุพร้อมกันเป็นหมู่คณะ (Mass Expiration)
val jitter = Random.nextInt(0, 300) // สุ่ม 0 ถึง 5 นาที
val ttl = 3600 + jitter
redis.set(key, data, ttl)
การกระจายเวลาแบบนี้จะทำให้ของแต่ละชิ้นทยอยหมดอายุคนละช่วงเวลา ฝูงชนจึงกระจายตัว ไม่ทะลักมารุมฐานข้อมูลในวินาทีเดียวกันอีกต่อไป
💡 5-Min Takeaway
"Cache ที่ดี ต้องคิดถึงวินาทีที่มันไม่มีอยู่ด้วย" การป้องกัน Thundering Herd ไม่ใช่การขยายเครื่อง Database ให้ใหญ่ขึ้นเพื่อทนแรงกระแทก แต่คือการควบคุมการไหลของ Request ไม่ให้พุ่งชน Database พร้อมกันในเสี้ยววินาทีที่ Cache Miss