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

Delivery Guarantees

ทุก messaging system มี delivery guarantee แบบใดแบบหนึ่ง มีสามแบบที่คุณจะได้ยิน:

Guaranteeความหมายความเสี่ยง
At-most-onceแต่ละ message ถูกส่ง 0 หรือ 1 ครั้งหาย — message หายได้
At-least-onceแต่ละ message ถูกส่ง 1 ครั้งขึ้นไปซ้ำ — message ซ้ำได้
Exactly-onceแต่ละ message ถูกส่งพอดี 1 ครั้ง(ความฝัน)

ความจริงที่น่าอึดอัด: ในระบบ distributed ที่มี network และ crash exactly-once delivery ทำไม่ได้ เป็น myth ดังนั้นทางเลือกจริง ๆ ในการออกแบบคือระหว่าง at-most-once กับ at-least-once — คุณกำลังเลือกว่าจะทน failure แบบไหนได้: message หาย หรือเห็น message ซ้ำสองครั้ง

ลองนึกภาพจังหวะที่ consumer ทำงานเสร็จ ตรงนั้นมีสองอย่างที่ต้องทำ: process message และ acknowledge crash เกิดขึ้น ระหว่าง สองอย่างนั้นได้ และไม่มีทางทำให้ทั้งคู่เป็น atomic ข้าม network:

sequenceDiagram
  participant Q as Queue
  participant C as Consumer
  Q->>C: deliver message
  C->>C: process (do the work)
  Note over C: crash HERE, before ack
  Note over Q: no ack arrived → redeliver
  Q->>C: deliver AGAIN (duplicate)
ช่องว่างที่เลี่ยงไม่ได้ที่ทำให้เกิด duplicate

ถ้า consumer crash หลัง ทำงานเสร็จแต่ ก่อน ack ถึง RabbitMQ จะ (อย่างถูกต้องและปลอดภัย) สมมติว่างานไม่เคยเกิดขึ้นและ redeliver งานรันสองครั้ง ไม่มีความฉลาดของ broker ใดลบช่องว่างนี้ได้ — process กับ ack ไม่ใช่ขั้นตอน atomic เดียว

นั่นคือเหตุผลที่ at-least-once เป็น default ที่ซื่อสัตย์และปลอดภัย: RabbitMQ ยอม deliver สองครั้งดีกว่าเสี่ยงทำ message หาย เลือก at-most-once (auto-ack, ไม่มี persistence) เฉพาะตอนที่ message หายนาน ๆ ทีไม่สำคัญจริง ๆ — metrics, live dashboard, ephemeral event

ข่าวดี: จริง ๆ คุณไม่ต้องการ exactly-once delivery คุณต้องการ exactly-once effect และคุณได้สิ่งนั้นด้วยการทำให้ consumer idempotent — process message เดิมสองครั้งให้ผลเหมือน process ครั้งเดียว

วิธี idempotent ที่ใช้ได้จริง:

  • Idempotency key ให้แต่ละ message มี unique id ที่เสถียร (message_id หรือ business key อย่าง order_id) ก่อนทำอะไรก็บันทึก “ฉันจัดการ id X แล้ว” ลง database (unique constraint, upsert, SETNX) duplicate จะเจอว่า key มีอยู่แล้วและไม่ทำอะไร
  • operation ที่ idempotent โดยธรรมชาติ “set status = shipped” รันซ้ำสองครั้งปลอดภัย ส่วน “เพิ่ม balance 10” ไม่ปลอดภัย เลือก set/upsert แทน increment ตรง ๆ เมื่อทำได้
  • dedup ตอน write ใช้ unique constraint ของ database บน business key เพื่อให้ duplicate insert fail แบบไม่มีอันตราย

รวม at-least-once delivery ของ RabbitMQ กับ idempotent consumer แล้วคุณได้ effectively-once processing — message อาจมาถึงสองครั้ง แต่ มีผล แค่ครั้งเดียว นั่นคือเป้าที่ถูกต้องและทำได้จริง

RabbitMQ เลือก delivery guarantee แบบใดเป็น default และความเสี่ยงของตัวเองคืออะไร?
ทำไม exactly-once delivery ถึงถือว่าเป็น myth?
"effectively-once" processing คืออะไร?
operation ใด idempotent โดยธรรมชาติมากที่สุด?