แคชทำงานปกติ แต่ก็ยังทำให้เกิดการเรียก API ซ้ำซ้อน

แคชไม่ได้เสีย

แต่ถึงอย่างนั้น การส่งคำขอ (request) พร้อมกันสามครั้งสำหรับชื่อผู้ใช้เดียวกัน กลับทำให้มีการเรียกไปยัง GitHub ถึงสามครั้ง

เหตุการณ์นี้เกิดขึ้นใน CommitPulse ซึ่งเป็น Next.js API ที่เปลี่ยนข้อมูลจาก GitHub ให้เป็น SVG badges เมื่อไฟล์ README กลายเป็นไวรัล ผู้คนนับพันจะเข้ามาดู badge พร้อมๆ กัน ซึ่งทำให้เกิดทราฟฟิกมหาศาล

แคชทำงานได้ดีสำหรับการเรียกแบบเรียงลำดับ (sequential) แต่กลับล้มเหลวเมื่อเป็นการเรียกแบบพร้อมกัน (concurrent)

ปัญหาก็คือ:

  • Request A ตรวจสอบแคชแล้วพบว่าไม่มีข้อมูล (cache miss) Request A จึงเริ่มดึงข้อมูลจาก GitHub
  • Request B ตามมาในอีก 5ms ต่อมา เมื่อตรวจสอบแคชก็ยังไม่พบข้อมูล เพราะ Request A ยังทำงานไม่เสร็จ Request B จึงเริ่มดึงข้อมูลเป็นครั้งที่สอง
  • Request C ตามมาในอีก 10ms ต่อมา ก็พบว่าแคชว่างเปล่าเช่นกัน และเริ่มดึงข้อมูลเป็นครั้งที่สาม

นี่คือปัญหา "thundering herd" การเกิด cache miss ภายใต้โหลดที่สูงจะกระตุ้นให้เกิดการเรียกคำขอที่เหมือนกันจำนวนมหาศาลไปยังผู้ให้บริการต้นทาง (upstream provider) หากคุณใช้ API ที่มีการจำกัดจำนวนครั้งในการเรียก (rate-limited) อย่าง GitHub สิ่งนี้อาจทำให้โควตาของคุณหมดลงในทันที

วิธีแก้คือการทำ request coalescing

คุณต้องติดตามคำขอที่กำลังดำเนินการอยู่ (pending requests) แยกต่างหากจากข้อมูลในแคชที่เสร็จสมบูรณ์แล้ว ผมจึงได้นำ Map สำหรับจัดการ "in-flight" requests มาใช้ดังนี้:

  • เมื่อคำขอเริ่มต้น ให้เก็บ Promise ของคำขอนั้นไว้ใน Map
  • หากมีคำขอที่สองส่งมาด้วย key เดียวกัน อย่าเริ่มการดึงข้อมูลใหม่
  • แต่ให้ส่งคืน Promise เดิมที่มีอยู่ใน Map แทน
  • เมื่อคำขอเสร็จสิ้น ให้ลบออกจาก Map และบันทึกผลลัพธ์ลงในแคช

วิธีนี้ช่วยให้มั่นใจได้ว่า ไม่ว่าจะมีคนเรียกขอข้อมูลชุดเดียวกันพร้อมกันกี่คน ก็จะมีเพียงการเรียก API เพียงครั้งเดียวเท่านั้น

ในระหว่างการแก้ไขปัญหานี้ ผมยังได้แก้บั๊กกรณีขอบ (edge-case) อื่นๆ อีกสามอย่างในไฟล์เดียวกันด้วย:

  • Missing Token Errors: ระบบส่งค่า "undefined" ไปเป็นข้อมูลยืนยันตัวตน (credential) ผมจึงอัปเดตให้มีการตรวจสอบ token ก่อนที่จะทำการส่งคำขอ
  • Memory Leaks: ตรรกะการลองใหม่ (retry logic) ทิ้ง event listeners ที่ค้างอยู่ไว้บน AbortSignals ผมจึงเพิ่มตรรกะการล้างข้อมูล (cleanup logic) เพื่อป้องกันการรั่วไหลของหน่วยความจำ
  • URL Injection: ชื่อผู้ใช้ที่มีอักขระพิเศษทำให้เส้นทาง API (API paths) เสียหาย ผมจึงเพิ่มการทำ encoding เพื่อปกป้องโครงสร้าง URL

แคชอย่างเดียวไม่พอ คุณยังต้องทำ deduplicate สำหรับคำขอที่กำลังดำเนินการอยู่ (in flight) ด้วย

แหล่งที่มา: https://dev.to/eshaanagrawal/the-cache-was-working-and-still-causing-duplicate-api-calls-3n51