การออกแบบการส่งซ้ำ・ลำดับ・idempotency
สำหรับนักพัฒนาที่อิมพลิเมนต์ฝั่งรับ Webhook บทความนี้อธิบายวิธีสร้างฝั่งรับที่ทำงานถูกต้องในสถานการณ์อย่าง "อีเวนต์เดียวกันมาถึงหลายครั้ง" หรือ "อีเวนต์เก่ามาถึงหลังอีเวนต์ใหม่"
Webhook เป็นกลไกที่สะดวก แต่เครือข่ายไม่เสถียร และเซิร์ฟเวอร์ก็มีโอกาสล่ม ReceiptRoller มีกลไกที่ส่งซ้ำอัตโนมัติเมื่อ "ส่งไม่ถึง" แต่ผลที่ตามมาก็คืออาจเกิดอีเวนต์เดียวกันมาถึง 2 ครั้งขึ้นไปและลำดับสลับหน้าหลังได้ ฝั่งรับควรออกแบบโดยตั้งสมมติฐานนี้ไว้ล่วงหน้า
นโยบายการส่งซ้ำ
หากฝั่งรับไม่คืน 2xx ReceiptRoller จะพยายามส่งซ้ำแบบ exponential backoff
| ครั้งที่ | จังหวะการส่ง |
|---|---|
| ครั้งที่ 1 | ทันที |
| ครั้งที่ 2 | หลัง 1 นาที |
| ครั้งที่ 3 | หลัง 5 นาที |
| ครั้งที่ 4 | หลัง 30 นาที |
| ครั้งที่ 5 | หลัง 2 ชั่วโมง |
| ครั้งที่ 6 | หลัง 6 ชั่วโมง |
| ครั้งที่ 7 | หลัง 24 ชั่วโมง |
รวมแล้วส่งซ้ำสูงสุด 7 ครั้งเป็นระยะเวลาราว 32 ชั่วโมง หากยังไม่สำเร็จ จะถูกบันทึกเป็นdead letter และตรวจสอบได้ที่ "ประวัติการส่ง Webhook" ในพอร์ทัลนักพัฒนา
เรสปอนส์ที่เข้าเงื่อนไขการส่งซ้ำ
5xx(เซิร์ฟเวอร์เออเรอร์)429(rate limit)- ไทม์เอาต์(ไม่ตอบสนองภายใน 10 วินาที)
- เออเรอร์การเชื่อมต่อ(DNS / TLS / TCP)
เรสปอนส์ที่ไม่เข้าเงื่อนไขการส่งซ้ำ
2xx:ถือว่าสำเร็จ ไม่ส่งซ้ำ3xx:ไม่ตามรีไดเรกต์อัตโนมัติ(ถือว่าล้มเหลวแต่ก็ไม่ส่งซ้ำ)4xx(รวม 401/410):ถือว่าเป็นการปฏิเสธถาวรของฝั่งรับ ไม่ส่งซ้ำ
5xx หากคืน 4xx จะไม่ถูกส่งซ้ำ
ลำดับไม่ถูกรับประกัน
Webhook ของ ReceiptRoller ไม่รับประกันลำดับการส่ง ตัวอย่างเช่นสถานการณ์ต่อไปนี้อาจเกิดขึ้นได้
receipt.refundedมาถึงก่อนreceipt.issuedproduct.updatedมาถึงก่อนproduct.created- การส่งครั้งที่ 1 ถูกส่งซ้ำ ทำให้อีเวนต์เก่ามาถึงหลังอีเวนต์ใหม่
ในงานที่ต้องคำนึงถึงลำดับ โปรดออกแบบให้ดู occurred_at(เวลาที่อีเวนต์เกิดขึ้น)ที่อยู่ในเพย์โหลด แล้วเรียงลำดับใหม่ที่ฝั่งรับ หรือดึงสถานะล่าสุดใหม่ผ่าน API การมอง Webhook เป็น "ทริกเกอร์ว่ามีการเปลี่ยนแปลง" แทนที่จะเป็น "การแจ้งสถานะล่าสุด" จะปลอดภัยกว่า
การอิมพลิเมนต์ idempotency
การทำให้ผลลัพธ์ไม่เปลี่ยนแม้อีเวนต์เดียวกันมาถึงหลายครั้ง เรียกว่าidempotency ReceiptRoller แนบ event_id ที่ไม่ซ้ำ(และเฮดเดอร์ X-RR-Event-Id)ให้แต่ละอีเวนต์ โปรดใช้สิ่งนี้เป็นคีย์เพื่อหลีกเลี่ยงการประมวลผลซ้ำ
แพตเทิร์น 1:บันทึก ID ที่ประมวลผลแล้ว
async function handleWebhook(event) {
// หากประมวลผลไปแล้วให้เพิกเฉย
const exists = await db.processedEvents.findOne({ event_id: event.event_id });
if (exists) return;
// การประมวลผลทางธุรกิจ
await processEvent(event);
// บันทึก(ในทรานแซกชันเดียวกับการประมวลผลทางธุรกิจจะดีที่สุด)
await db.processedEvents.insert({
event_id: event.event_id,
received_at: new Date(),
});
}
ตั้ง TTL ให้ตารางอีเวนต์ที่ประมวลผลแล้ว เพื่อลบเรกคอร์ดเก่าอัตโนมัติ จะช่วยป้องกันไม่ให้ข้อมูลบวมขึ้น(เมื่อคำนึงว่าช่วงส่งซ้ำคือ 32 ชั่วโมง ควรเก็บไว้อย่างน้อย 7 วัน)
แพตเทิร์น 2:เขียนด้วย UPSERT
หากการประมวลผลทางธุรกิจเป็นแบบ "ทำให้เรกคอร์ดหนึ่งกลายเป็นสถานะล่าสุด" การใช้ UPSERT(INSERT or UPDATE)จะทำให้เป็น idempotent โดยธรรมชาติ
// รับอีเวนต์สต็อก แล้ว UPSERT โดยใช้ product ID เป็นคีย์
await db.inventory.upsert({
where: { product_id: event.data.product_id },
update: { quantity: event.data.quantity, updated_at: event.occurred_at },
create: { product_id: event.data.product_id, quantity: event.data.quantity },
});
ในแพตเทิร์นนี้ ต้องใส่การเปรียบเทียบ occurred_at เพื่อไม่ให้เขียนทับค่าล่าสุดในกรณีที่อีเวนต์เก่ามาถึงทีหลัง
แพตเทิร์น 3:ตัดสินด้วยคีย์ธรรมชาติ
หากรู้คีย์ธรรมชาติทางธุรกิจ(เช่น receipt_id)ก็สามารถตรวจจับการซ้ำด้วยคีย์นั้นได้อย่างเรียบง่าย การใช้ร่วมกับแพตเทิร์นที่ใช้ event_id จะทำให้ทนทานยิ่งขึ้น
การจัดการเมื่อลำดับสลับ
มาตรการทั่วไปกรณีที่หลายอีเวนต์ต่อเอนทิตีเดียวกันมาถึงสลับลำดับ
- ตัดสินการเขียนทับด้วย occurred_at:หากเป็นอีเวนต์ที่เก่ากว่าเวลาอัปเดตล่าสุดที่ฝั่งรับถืออยู่ ก็ไม่นำมาใช้
- หมายเลขเวอร์ชัน:หากในเพย์โหลดมี
versionให้เลือกใช้เฉพาะค่าที่มากกว่า - ตรวจสอบการเปลี่ยนสถานะ:เพิกเฉยต่อการเปลี่ยนสถานะที่เป็นไปไม่ได้ทางธุรกิจ เช่น "refunded → issued"
- ดึงข้อมูลใหม่ผ่าน API:เมื่อได้รับการแจ้ง ให้ดึงสถานะล่าสุดใหม่ผ่าน API(จบได้โดยไม่ต้องคำนึงถึงลำดับ)
การจัดการ dead letter
อีเวนต์ที่ส่งซ้ำ 7 ครั้งแล้วยังไม่สำเร็จ จะถูกบันทึกเป็น "dead letter" และตรวจสอบได้ที่ พอร์ทัลนักพัฒนา → เอนด์พอยต์ที่เกี่ยวข้อง → แท็บ "ประวัติการส่ง" แต่ละเรกคอร์ดจะมีข้อมูลต่อไปนี้
- อีเวนต์ ID, ชนิด, ไทม์สแตมป์
- เรสปอนส์โค้ดและบอดี ของแต่ละครั้งที่ลอง(1KB แรก)
- ปุ่ม "ส่งซ้ำ"
หลังแก้ไขฝั่งรับแล้ว สามารถส่งซ้ำด้วยมือด้วยปุ่ม "ส่งซ้ำ" ได้ ระยะเวลาคือ 30 วันนับจากการส่ง
แอนตี้แพตเทิร์นที่พบบ่อย
| การอิมพลิเมนต์ที่มักผิดพลาด | ปัญหา |
|---|---|
เพราะประมวลผลหนัก จึงรอจนประมวลผลเสร็จ ไม่คืน 2xx หลังโยนเข้าคิวแบบอะซิงโครนัส |
เข้าลูปส่งซ้ำเพราะไทม์เอาต์ 10 วินาที |
คืน 200 ตอนที่การประมวลผลทางธุรกิจล้มเหลว |
ไม่ถูกส่งซ้ำ ทำให้อีเวนต์หลุดหาย |
| บวก/ลบสต็อกโดยไม่มี idempotency | จำนวนสต็อกเพี้ยนเพราะการส่งซ้ำ |
| เขียนทับแฟล็กล่าสุดโดยไม่คำนึงถึงลำดับที่สลับ | อีเวนต์เก่าไปเหยียบทับสถานะใหม่ |
| ไม่ได้มอนิเตอร์ dead letter | ผ่านไปหลายวันโดยไม่รู้ตัวว่าเกิดเหตุขัดข้อง |
แพตเทิร์นการรับที่แนะนำ
- ตรวจสอบลายเซ็น・ไทม์สแตมป์
- ตรวจสอบ idempotency ด้วย
event_id(หากประมวลผลแล้วคืน200ทันที) - โยนเพย์โหลดเข้าคิวภายใน(SQS / Cloud Tasks เป็นต้น)
- คืน
200ทันที(ตรงนี้เป้าหมายคือภายใน 1 วินาที) - ประมวลผลทางธุรกิจที่คิวเวิร์กเกอร์(รีทรายทำภายใน)
ด้วยแพตเทิร์นนี้ เอนด์พอยต์รับจะตอบสนองได้เร็วอยู่เสมอ และความล้มเหลวของการประมวลผลทางธุรกิจจะไม่นำไปสู่ลูปส่งซ้ำ
คู่มือที่เกี่ยวข้อง
-
สารบัญความช่วยเหลือสำหรับนักพัฒนาสารบัญความช่วยเหลือสำหรับนักพัฒนา ReceiptRoller รวบรวมตั้งแต่การสมัครเป็นนักพัฒนา การลงทะเบียนแอปพลิเคชัน การยืนยันตัวตน OAuth และสโคป คู่มือการพัฒนา (แอปวอลเล็ต, Webhook สำหรับร้านค้า, Survey API) คู่มือแยกตามโดเมนข้อมูล การใช้งานและความปลอดภัย คอมมูนิตี ไปจนถึงการแก้ปัญหา
-
วิธีลงทะเบียน Webhookอธิบายขั้นตอนการลงทะเบียน Webhook endpoint ในพอร์ทัลนักพัฒนาของ ReceiptRoller วิธีเลือกอีเวนต์ที่ต้องการสมัครรับ การส่งทดสอบ การแบ่งใช้หลาย endpoint และวิธีลบหรือปิดใช้งานชั่วคราว
-
การมอนิเตอร์และการจัดการเมื่อล้มเหลวอธิบายวิธีดูประวัติการส่ง Webhook ของ ReceiptRoller, ตัวชี้วัดที่ควรมอนิเตอร์และการออกแบบอะเลิร์ต, การส่ง dead letter ซ้ำ, รูปแบบเหตุขัดข้องที่พบบ่อยและขั้นตอนการกู้คืน
-
การตรวจสอบลายเซ็นและความปลอดภัยอธิบายกลไกการตรวจสอบลายเซ็น HMAC-SHA256 ของ Webhook ใน ReceiptRoller ตัวอย่างโค้ดตรวจสอบ (Node.js / Python / C#) มาตรการป้องกัน replay attack และวิธีจัดการ secret อย่างปลอดภัย
-
ภาพรวมของ Webhookอธิบายชนิดอีเวนต์หลักที่ Webhook ของ ReceiptRoller ส่งมา เหตุผลที่ควรใช้ Webhook แทน polling รูปแบบการส่ง (HTTPS POST + JSON) และแนวคิดเรื่องการรับประกันการส่ง