Event ID ไม่ตรงกัน ทำให้ระบบ Dedup พังทั้งสองทาง: นับซ้ำหรือหายไปเลย

สรุปสั้น ๆ
Deduplication key หรือ event ID คือกลไกที่บอกปลายทางว่า event นี้เคยเห็นแล้วหรือยัง ถ้าออกแบบผิด จะเกิดปัญหาได้สองแบบตรงข้ามกัน คือนับซ้ำ (ถ้า ID ไม่ซ้ำทั้งที่ควรซ้ำ) หรือยอดหาย (ถ้า ID ซ้ำกันทั้งที่ควรต่างกัน) หลักการที่ถูกต้องคือ ID ต้องคำนวณจากข้อมูลของออเดอร์เอง ไม่ใช่ค่าสุ่มหรือเวลาปัจจุบัน
ทีมพัฒนาระบบหนึ่งเจอปัญหาแปลกที่ทำให้งงอยู่พักหนึ่ง คือลูกค้าสองคนที่ปิดการขายห่างกันแค่ไม่กี่วินาทีในช่วงเวลาเดียวกัน ถูกระบบนับเป็น conversion แค่รายการเดียว อีกรายหายไปเลย ทั้งที่เป็นออเดอร์คนละคน คนละสินค้ากันจริง ๆ
หลังไล่ดูโค้ดจึงพบว่าต้นตอคือการสร้าง event ID จากการรวมชื่อสินค้ากับเวลาปัจจุบันแบบปัดเศษเป็นวินาที พอมีสองออเดอร์เกิดขึ้นในวินาทีเดียวกันพอดี ID ที่คำนวณออกมาจึงซ้ำกันโดยบังเอิญ ทำให้ปลายทางเข้าใจว่าเป็น event เดียวกันแล้วรับแค่รายการแรก
Event ID หรือ Deduplication Key คืออะไรกันแน่
หลักการง่าย ๆ คือทุก event ที่ส่งเข้าไปต้องมีรหัสเฉพาะตัวติดไปด้วย เพื่อให้ปลายทางเช็คได้ว่าเคยเห็นรหัสนี้มาก่อนหรือยัง ถ้าเคยเห็นแล้วก็จะข้ามไม่บันทึกซ้ำ กลไกนี้คือสิ่งที่ป้องกันการนับซ้ำเวลามีการ retry หรือส่งข้อมูลซ้ำโดยไม่ตั้งใจ
แต่กลไกนี้จะทำงานถูกต้องก็ต่อเมื่อ ID ถูกออกแบบมาให้ ‘เหมือนกันเสมอสำหรับเหตุการณ์เดียวกัน’ และ ‘ต่างกันเสมอสำหรับเหตุการณ์ที่ต่างกัน’ ถ้าออกแบบผิดพลาดไปในทางใดทางหนึ่ง ปัญหาที่ตามมาจะตรงข้ามกันโดยสิ้นเชิง
สองรูปแบบความล้มเหลวที่เกิดจากจุดเดียวกัน
- ID ไม่ซ้ำทั้งที่ควรซ้ำ — เช่นใช้เวลาปัจจุบันหรือค่าสุ่มเป็นส่วนหนึ่งของ ID ทำให้ทุกครั้งที่ retry event เดิม ID เปลี่ยนไปเรื่อย ๆ ปลายทางจึงไม่รู้ว่าซ้ำ นำไปสู่ปัญหานับซ้ำ
- ID ซ้ำกันทั้งที่ควรต่างกัน — เช่นคำนวณจากข้อมูลที่หยาบเกินไป (อย่างชื่อสินค้ากับเวลาปัดเศษเป็นวินาที) ทำให้เหตุการณ์คนละเหตุการณ์บังเอิญได้ ID เดียวกัน ปลายทางจึงคิดว่าซ้ำแล้วปฏิเสธรายการที่สอง ทำให้ยอดหาย
ตัวอย่างการออกแบบที่ดีเทียบกับที่มีปัญหา
| วิธีออกแบบ ID | ปัญหาที่มักเจอ | ควรใช้หรือไม่ |
|---|---|---|
| ค่าสุ่ม (random UUID) ทุกครั้งที่ยิง | retry แล้ว ID เปลี่ยน ทำให้นับซ้ำ | ไม่ควรใช้เดี่ยว ๆ |
| เวลาปัจจุบันปัดเศษเป็นวินาที | เหตุการณ์พร้อมกันชนกัน ทำให้ยอดหาย | ไม่ควรใช้ |
| รหัสออเดอร์ + ประเภทเหตุการณ์ | แทบไม่มีปัญหา เพราะผูกกับข้อมูลจริงที่ไม่ซ้ำ | แนะนำให้ใช้ |
| รหัสออเดอร์ + ประเภทเหตุการณ์ + เวอร์ชันข้อมูล | รองรับกรณีต้องอัปเดตข้อมูล event เดิมด้วย | แนะนำสำหรับระบบที่ซับซ้อนขึ้น |
ขั้นตอนออกแบบ Event ID ให้ถูกต้องตั้งแต่ต้น
- ระบุข้อมูลที่ไม่ซ้ำกันจริงของแต่ละเหตุการณ์ เช่นรหัสออเดอร์ที่ระบบสร้างขึ้นเองอยู่แล้ว แทนที่จะพึ่งเวลาหรือค่าสุ่ม
- รวมประเภทของเหตุการณ์เข้าไปด้วย เช่น ‘purchase’ หรือ ‘lead’ เพื่อให้ ID ต่างกันถ้าออเดอร์เดียวกันมีหลายเหตุการณ์เกิดขึ้น
- ทดสอบด้วยการจำลองสองเหตุการณ์ที่เกิดขึ้นพร้อมกันจริง ๆ (ไม่ใช่แค่ทดสอบทีละตัว) เพื่อยืนยันว่า ID ที่ได้ไม่ชนกัน ผ่านtest event toolของปลายทาง
- ทดสอบการ retry event เดิมซ้ำหลายครั้ง เพื่อยืนยันว่า ID ที่ได้เหมือนเดิมทุกครั้ง ไม่เปลี่ยนไปตามเวลาที่ยิง กันไม่ให้เกิดปัญหาจาก webhook ที่ยิงซ้ำตามมา
- เอกสารบันทึกวิธีคำนวณ ID ไว้ให้ทีมอื่นเข้าใจตรงกัน เพื่อไม่ให้มีการเปลี่ยนวิธีคำนวณโดยไม่รู้ผลกระทบภายหลัง
สรุป
event ID ที่ออกแบบดีคือสิ่งที่ป้องกันปัญหาได้ตั้งแต่ต้นทาง ทั้งปัญหานับซ้ำและปัญหายอดหายที่ดูเหมือนเป็นคนละเรื่องกัน แต่จริง ๆ แล้วมีรากมาจากจุดเดียวกัน
การลงทุนเวลาออกแบบ ID ให้ถูกต้องตั้งแต่แรกใช้เวลาไม่มาก แต่ประหยัดเวลาไล่ดีบักปัญหาที่ซับซ้อนกว่ามากในภายหลัง
- event ID ต้องเหมือนกันเสมอสำหรับเหตุการณ์เดียวกัน และต่างกันเสมอสำหรับเหตุการณ์ต่างกัน
- อย่าใช้ค่าสุ่มหรือเวลาปัจจุบันปัดเศษเป็นส่วนประกอบหลักของ ID
- ใช้รหัสออเดอร์ร่วมกับประเภทเหตุการณ์เป็นฐานในการคำนวณ ID
- ทดสอบทั้งกรณี retry ซ้ำและกรณีเหตุการณ์พร้อมกันก่อนใช้งานจริง
คำถามที่พบบ่อย
ทำไมใช้ค่าสุ่มถึงไม่ปลอดภัยสำหรับ event ID ทั้งที่ค่าสุ่มไม่ค่อยชนกัน
ประเด็นไม่ได้อยู่ที่ค่าสุ่มชนกันเอง แต่อยู่ที่ค่าสุ่มเปลี่ยนไปทุกครั้งที่ระบบพยายามส่งใหม่ (retry) ทำให้ปลายทางไม่มีทางรู้ว่านี่คือความพยายามส่งซ้ำของเหตุการณ์เดิม จึงรับไว้เป็นสองรายการแยกกัน
ควรใช้รหัสออเดอร์เพียงอย่างเดียวเป็น event ID ได้เลยไหม
ได้ถ้าออเดอร์หนึ่งมีแค่หนึ่งเหตุการณ์ที่ต้องส่ง แต่ถ้าออเดอร์เดียวอาจมีหลายเหตุการณ์ (เช่นทั้งเหตุการณ์เพิ่มสินค้าและเหตุการณ์ชำระเงิน) ควรรวมประเภทเหตุการณ์เข้าไปด้วยเพื่อแยกความแตกต่างให้ชัดเจน
ตรวจสอบยังไงว่าระบบปัจจุบันมีปัญหาการออกแบบ ID แบบนี้อยู่หรือเปล่า
ลองสุ่มตรวจ event ที่เกิดขึ้นในช่วงเวลาใกล้เคียงกันมาก ๆ (ห่างกันไม่ถึงวินาที) แล้วดูว่า ID ที่คำนวณได้ต่างกันจริงหรือไม่ ถ้าพบว่ามี ID ซ้ำกันทั้งที่เป็นคนละเหตุการณ์ นั่นคือสัญญาณว่าการออกแบบมีปัญหา
ปัญหานี้กระทบกับ event ประเภทไหนมากที่สุด
กระทบมากที่สุดกับธุรกิจที่มีปริมาณ event สูงและเกิดขึ้นถี่ในเวลาใกล้เคียงกัน เช่นธุรกิจที่มีลูกค้าปิดการขายพร้อมกันหลายรายในช่วงเวลาสั้น ๆ อย่างช่วงแคมเปญโปรโมชันใหญ่ ยิ่งปริมาณสูงยิ่งมีโอกาสชนกันมากขึ้น
ถ้าพบว่าออกแบบผิดมาตั้งแต่ต้น ควรแก้ทันทีหรือรอจังหวะที่เหมาะสม
ควรแก้โดยเร็วที่สุดเท่าที่ทำได้ เพราะยิ่งปล่อยไว้นาน ข้อมูลที่คลาดเคลื่อนสะสมยิ่งมากขึ้นเรื่อย ๆ และกระทบการตัดสินใจด้านงบโฆษณาต่อเนื่อง แต่ควรทดสอบให้รอบคอบก่อนเปลี่ยนวิธีคำนวณ ID เพราะการเปลี่ยนกลางทางอาจทำให้ event เดิมที่เคยส่งไปแล้วกับ event ใหม่ไม่ต่อเนื่องกัน
บทความที่เกี่ยวข้อง


