ตัดเงินแล้วแต่ตัดสต็อกไม่ผ่าน: ทำไม Two-Phase Commit ถึงพาไมโครเซอร์วิสกอดคอกันตายหมู่?
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 30 วินาที
ในยุคแอป Monolith การซื้อของเป็นเรื่องง่ายดาย: ครอบโค้ดด้วย @Transactional บรรทัดเดียว ตัดเงินในตาราง A ตัดสต็อกในตาราง B ถ้าพังตรงไหน Database จะช่วย ROLLBACK ข้อมูลคืนให้สะอาดหมดจด
แต่วันนี้ระบบของคุณแตกกระจายเป็น Microservices:
- Order Service ดูแลคำสั่งซื้อ (Database 1)
- Payment Service หักเงินในบัญชีลูกค้า (Database 2)
- Inventory Service ตัดสต็อกในโกดัง (Database 3)
โจทย์คือ: "ถ้าหักเงินไปแล้ว 1,000 บาท... แต่พอจะไปตัดสต็อก ปรากฏว่าของหมดพอดี! เราจะเอาเงินคืนลูกค้าและยกเลิกออเดอร์ข้าม 3 ฐานข้อมูลนี้ได้อย่างไร โดยไม่มีคำสั่ง ACID Transaction แบบเดิม?"
⛓️ โซ่ตรวนมรณะ: Two-Phase Commit (2PC)
วิศวกรยุคเก่ามักคิดถึง Two-Phase Commit (2PC / XA Transaction) โดยมีตัวกลาง (Coordinator) คอยคุมบังเหียน 2 สเต็ป:
- Phase 1 (Prepare): ผู้จัดการถามทุกเซอร์วิส: "ทุกคนพร้อมบันทึกข้อมูลไหม?" ทั้ง 3 เซอร์วิสจะสั่งล็อกแถวข้อมูล (Lock Row) ใน Database ของตัวเองไว้แน่นหนา แล้วตะโกนตอบว่า "พร้อม!"
- Phase 2 (Commit): ถ้าทุกคนตอบพร้อม ผู้จัดการจะสั่ง: "ทุกคน Commit ได้!"
"แต่จุดวิบัติคือ: ในระหว่างที่ทุกคนถือ Lock ไว้ใน Phase 1 เพื่อรอคำสั่ง Commit... ถ้าเกิดเน็ตเวิร์กสะอึก หรือตัว Coordinator ดับวูบไป ทุก Database จะติดสถานะ 'แช่แข็ง (Blocking Lock)' ค้างเติ่งไว้ตลอดกาล คนอื่นห้ามแตะสต็อก ห้ามแตะยอดเงิน ทราฟฟิกทั้งระบบจะสำลักตายในพริบตา!"
🎭 ทางรอดแห่งโลกกระจายศูนย์: The Saga Pattern
ในระบบ Distributed Systems ระดับโลกอย่าง Netflix หรือ Uber ไม่มีใครใช้ 2PC อีกต่อไป... พวกเขาเปลี่ยนมาใช้ Saga Pattern
หัวใจของ Saga คือ: "เลิกบังคับให้ทุกอย่างเสร็จพร้อมกันในวินาทีเดียว (ACID) แต่ยอมรับความจริงแบบเสร็จทีละขั้น (Eventual Consistency)"
การทำงานจะแบ่งเป็น Transaction ย่อยๆ ที่ทำเสร็จแล้ว Commit ทันทีทีละตัว:
- 1. Order Service สร้างออเดอร์สถานะ
PENDING✅ (จบงาน ไม่ล็อกค้าง) - 2. Payment Service หักเงินลูกค้าสำเร็จ ✅ (จบงาน)
- 3. Inventory Service ตรวจสอบแล้วพบว่า "ของหมด!" ❌
↩️ คาถาแก้กรรม: Compensating Transaction (ธุรกรรมชดเชย)
เมื่อสเต็ปที่ 3 ล้มเหลว ระบบจะ ไม่สามารถกด Ctrl+Z ย้อนเวลาได้ เพราะเงินถูกหักลง Database ไปแล้วจริงๆ!
สิ่งเดียวที่ทำได้คือการรัน "ธุรกรรมชดเชย (Compensating Transaction)" เดินถอยหลังทีละสเต็ปเพื่อหักล้างผลลัพธ์เดิม:
[Inventory] สต็อกหมด ➔ ยิง Event "STOCK_FAILED"
[Payment] ได้รับ Event ➔ สั่ง "โอนเงินคืน (Refund) 1,000 บาท"
[Order] ได้รับ Event ➔ อัปเดตสถานะเป็น "CANCELLED"
เหมือนเวลาเราจองตั๋วเครื่องบิน: ถ้าหักเงินสำเร็จแต่ดันออกตั๋วไม่ผ่าน สายการบินไม่ได้ย้อนเวลา แต่เขาส่งคำสั่ง "คืนเงินเข้าบัตรเครดิต" ให้คุณตามหลังแทน
🧭 2 รูปแบบของ Saga: Choreography vs Orchestration
- Choreography (เต้นรำตามจังหวะ): ไม่มีหัวหน้า แต่ละเซอร์วิสฟัง Event จาก Message Broker (Kafka/RabbitMQ) แล้วทำงานต่อกันเป็นทอดๆ ➔ เหมาะสำหรับงานไม่ซับซ้อน (2-4 เซอร์วิส)
- Orchestration (วาทยกรคุมวง): มีตัวกลางอย่าง Temporal, Camunda หรือ AWS Step Functions คอยสั่งการทีละสเต็ปว่า "A ทำเสร็จแล้วใช่ไหม... ต่อไป B ทำนะ ถ้า B พัง ให้กลับไปสั่งยกเลิก A" ➔ เหมาะสำหรับระบบซับซ้อนที่มีหลายสิบขั้นตอน
💡 5-Min Takeaway
"ในโลก Distributed การล็อกข้อมูลข้ามเครือข่ายคือยาพิษ แต่การยอมรับความจริงแล้วเตรียมแผนชดเชยคือทางรอด" จงหยุดพยายามสร้าง ACID Transaction ข้ามไมโครเซอร์วิสด้วย Two-Phase Commit ที่พร้อมจะล่มสลายยามเครือข่ายสะอึก หันมาใช้ Saga Pattern ร่วมกับ Compensating Transactions เพื่อให้ทุกเซอร์วิสทำงานได้อย่างอิสระ รวดเร็ว และสามารถฟื้นฟูความถูกต้องของข้อมูลกลับมาได้เสมอแม้ในวันที่เลวร้ายที่สุด