The Dual-Write Problem: เซฟลง DB แล้วยิงเข้า Kafka... ปัญหาโลกแตกที่ทำลายความจริงใจของระบบ
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 30 วินาที
ในสถาปัตยกรรม Microservices หรือ Event-Driven ยุคนี้ นี่คือโค้ด 2 บรรทัดที่ดูไร้พิษภัยที่สุด แต่คร่าชีวิตระบบการเงินมานับไม่ถ้วน:
kafkaTemplate.send("order-created", order) // สเต็ป 2: ยิง Event บอกคนอื่น
คำถามง่ายๆ: ถ้าบรรทัดแรกทำงานผ่าน แต่ก่อนจะเริ่มบรรทัดที่สอง... ปลั๊กเซิร์ฟเวอร์โดนเตะหลุดพอดี จะเกิดอะไรขึ้น?
ออเดอร์ถูกบันทึกลงฐานข้อมูลเรียบร้อย... แต่ข้อความไม่เคยถูกส่งเข้า Kafka แผนกตัดของในคลังไม่รู้เรื่อง ลูกค้าจ่ายเงินแต่ไม่เคยได้ของ! และต่อให้คุณสลับเอา Kafka ขึ้นก่อน ถ้า Kafka ส่งผ่านแล้ว DB ดับ ยิ่งบรรลัยหนักกว่าเดิม เพราะคลังตัดของแจกฟรีแต่ในระบบไม่มีประวัติซื้อ!
💔 ฝันร้ายของสาย Distributed: ไม่มีความรักที่พร้อมกันสองที่
ปัญหานี้ในวงการเรียกว่า The Dual-Write Problem (ปัญหาเขียนสองฝั่งพร้อมกัน)
Database ของคุณ (เช่น PostgreSQL หรือ MySQL) กับ Message Broker ของคุณ (เช่น Apache Kafka) เป็นคนละเทคโนโลยีที่ไม่ได้แชร์ Transaction ร่วมกัน
คุณไม่สามารถเอา @Transactional ของ Spring ไปครอบทั้งการบันทึกฐานข้อมูลและการยิง Kafka เพื่อให้มัน Rollback พร้อมกันได้ในชีวิตจริง เพราะโลก Distributed System ไม่มี Atomicity ข้ามระบบแบบง่ายๆ
📬 ท่าไม้ตายระดับตำนาน: Transactional Outbox Pattern
เพื่อแก้ปัญหานี้ วิศวกรซอฟต์แวร์เลยคิดค้นวิธีที่เรียบง่ายแต่ทรงพลัง โดยเปรียบเทียบเหมือน "ตู้จดหมายหน้าบ้าน (Outbox)":
"อย่าเพิ่งพยายามส่งจดหมายข้ามอำเภอ (Kafka) ในตอนนี้... จงเขียนจดหมายแล้วหย่อนลง 'ตู้จดหมายในบ้านตัวเอง' (ตารางใน DB เดียวกัน) เสียก่อน เพราะอะไรที่อยู่ในบ้านเดียวกัน เราคุมได้ 100%!"
ขั้นตอนการทำงานเปลี่ยนเป็นแบบนี้:
- สร้างตารางเพิ่มขึ้นมา 1 ตารางชื่อว่า
outbox_eventsในฐานข้อมูลเดิม - เวลาบันทึก Order ให้เขียนทั้งข้อมูล Order และข้อความ Event ลงตาราง
outbox_eventsใน Database Transaction เดียวกัน (ถ้าพังก็พังคู่ ถ้ารอดก็รอดคู่ 100%) - มีตัวช่วยส่งจดหมาย (เช่น Background Job หรือเครื่องมือ Change Data Capture อย่าง Debezium) คอยมาแอบอ่านตาราง
outbox_eventsแล้วหยิบข้อความไปยิงใส่ Kafka ให้ทีหลัง - พอยิงเข้า Kafka สำเร็จ ค่อยมาทำเครื่องหมายว่า
PROCESSEDหรือลบออกจากตาราง Outbox
@Transactional
fun createOrder(order: Order) {
orderRepository.save(order)
outboxRepository.save(OutboxEvent("ORDER_CREATED", order.toJson()))
}
💡 5-Min Takeaway
"อย่าพึ่งพาโชคชะตาเมื่อต้องบันทึกข้อมูลข้ามระบบเครือข่าย" โค้ดที่เรียกของสองอย่างต่อกันทาง Network มีโอกาสพังตรงกลางเสมอ การใช้ Transactional Outbox Pattern ช่วยเปลี่ยนงานเสี่ยงตายข้ามระบบ ให้กลายเป็นการันตีระดับ ACID ภายใน Database เดียวกัน แล้วค่อยใช้โมเดล At-least-once delivery ส่งต่อให้ระบบภายนอกอย่างปลอดภัย