Distributed Systems · บทที่ 5 จาก 11

Distributed Transactions

บทที่แล้วรับประกัน atomicity ได้บนเครื่องเดียว — แต่ถ้า transaction หนึ่งต้องอัปเดตข้อมูลบนหลายเครื่องพร้อมกันล่ะ? บทนี้คือคำตอบคลาสสิกของปัญหานั้น (Two-Phase Commit) พร้อมข้อจำกัดที่ทำให้วงการ microservices เลือกเดินเส้นทางอื่น

📚
อิงเนื้อหาจาก Coulouris บทที่ 14 (Distributed Transactions) — ต่อยอดโดยตรงจาก Transaction & Concurrency Control (บทที่ 4) โดยขยายให้ atomicity ครอบคลุมหลายเครื่อง เชื่อมโยงกับ Saga Pattern ในวิชา Network Computing หัวข้อที่ 4 อย่างใกล้ชิด

1Flat Transaction เทียบกับ Nested Transaction

รูปแบบลักษณะ
Flat transactionClient เรียก operation ไปยังหลาย server เรียงกันภายใต้ transaction เดียว ไม่มีโครงสร้างย่อยซ้อนกัน — ถ้า operation ใดล้มเหลว ทั้ง transaction ต้อง abort
Nested transactionTransaction แม่ (parent) แบ่งงานเป็น sub-transaction ลูกที่ทำงานอิสระกันได้ในระดับหนึ่ง — sub-transaction ลูกอาจ commit/abort เองได้โดยไม่กระทบทั้งหมดในทันที เปิดโอกาสให้ทำงานแบบ concurrent ระหว่าง sub-transaction มากขึ้น แต่ซับซ้อนกว่ามากในการ implement

2Atomic Commit Problem: ทำไม Commit ข้ามเครื่องถึงยาก

โจทย์
เมื่อ transaction กระทบข้อมูลบนหลาย server (แต่ละ server เรียกว่า participant) ทุก participant ต้องตกลงร่วมกันว่าจะ commit หรือ abort พร้อมกันทั้งหมด — จะให้ participant บางตัว commit และบางตัว abort ไม่ได้ (จะทำให้ atomicity เสียหาย)
ความยากที่แท้จริง
Participant แต่ละตัวรู้แค่ว่าตัวเองพร้อม commit หรือไม่ ไม่รู้ว่า participant อื่นพร้อมหรือเปล่า และการสื่อสารระหว่างพวกมันอาจล้มเหลวได้ทุกเมื่อ (ตรงกับ asynchronous distributed system ในบทที่ 1) — จึงต้องมี protocol พิเศษเพื่อประสานการตัดสินใจนี้

3Two-Phase Commit Protocol (2PC)

แนวคิด
แยกการตัดสินใจออกเป็น 2 phase ชัดเจน โดยมี coordinator หนึ่งตัวคุมกระบวนการ: Phase 1 (Voting/Prepare) ถามทุก participant ว่าพร้อม commit หรือไม่ และ Phase 2 (Commit) สั่งให้ทุกคน commit หรือ abort ตามผลโหวต
Phase 1 (canCommit?)
  Coordinator -> Participant A: canCommit?
  Coordinator -> Participant B: canCommit?
  Participant A -> Coordinator: Yes (เขียน prepare log แล้ว)
  Participant B -> Coordinator: Yes (เขียน prepare log แล้ว)

Phase 2 (doCommit)
  Coordinator: ทุกคนตอบ Yes -> ตัดสินใจ commit
  Coordinator -> Participant A: doCommit
  Coordinator -> Participant B: doCommit
  Participant A -> Coordinator: haveCommitted
  Participant B -> Coordinator: haveCommitted
กฎเหล็กของ Phase 1
เมื่อ participant ตอบ "Yes" แล้ว มันสัญญาว่าจะต้องสามารถ commit ได้จริงเมื่อถูกสั่งในภายหลัง ไม่ว่าจะเกิดอะไรขึ้น — จึงต้องเขียนข้อมูลลง log ถาวรก่อนตอบ Yes เสมอ (เผื่อระบบล่มระหว่างรอ phase 2) นี่คือที่มาของคำว่า participant อยู่ใน "in-doubt" / "uncertain" state ระหว่างรอคำสั่งจาก coordinator

เมื่อ Participant ตอบ No

ถ้ามี participant ใดตอบ No ในระหว่าง phase 1 (เช่น constraint ถูกละเมิด หรือ lock ขอไม่ได้) coordinator จะตัดสินใจ abort ทั้ง transaction ทันที แล้วส่ง doAbort ไปยังทุก participant ที่เหลือ

4ความล้มเหลวใน 2PC และปัญหา Blocking

สถานการณ์ล้มเหลวผลกระทบและการรับมือ
Participant ล่มก่อนตอบ Phase 1Coordinator รอไม่ได้คำตอบ ตัดสินใจ abort ทั้ง transaction (ปลอดภัย เพราะ participant ตัวนั้นยังไม่เคยสัญญาว่าจะ commit)
Participant ล่มหลังตอบ Yes แต่ก่อนได้รับ doCommitเมื่อฟื้นตัว participant ต้องดู log ว่าตัวเองเคยตอบ Yes ไปแล้ว แต่ไม่รู้ว่า coordinator ตัดสินใจ commit หรือ abort ไปแล้ว — ต้องถาม coordinator ใหม่ (recovery)
Coordinator ล่มระหว่าง Phase 2Participant ที่ตอบ Yes ไปแล้วจะค้างรอคำสั่งอยู่ ปล่อย lock ที่ถืออยู่ไม่ได้ (เพราะยังไม่รู้ผลตัดสิน) — เรียกว่า blocking problem ซึ่งเป็นจุดอ่อนที่มีชื่อเสียงที่สุดของ 2PC
Blocking Problem — จุดอ่อนสำคัญของ 2PC
ถ้า coordinator ล่มระหว่างที่ participant กำลังรอคำสั่งใน phase 2 participant นั้นจะค้างอยู่ในสถานะ "uncertain" ไม่รู้ว่าควร commit หรือ abort ต้องรอ coordinator ฟื้นตัวเท่านั้น (หรือติดต่อ participant ตัวอื่นเพื่อขอข้อมูล ถ้า protocol รองรับ) ระหว่างนั้น lock ที่ถืออยู่จะกันไม่ให้ transaction อื่นเข้าถึงข้อมูลเดียวกันได้เลย — นี่คือเหตุผลหลักที่ 2PC ถูกมองว่า "ไม่ scale" สำหรับระบบที่ต้องการ availability สูง

Distributed Transaction Recovery

กลไก
แต่ละ participant มี Recovery Manager ที่จัดการ log ไฟล์ — เก็บ checkpoint (ภาพรวมของค่าตั้งต้น เปรียบเหมือน snapshot) และบันทึกทุก operation ระหว่างทาง เมื่อ process ฟื้นตัวจากความล้มเหลว recovery manager ใช้ log เพื่อกู้สถานะกลับไปยังจุดที่ถูกต้องล่าสุด — ต่อ protocol 2PC ที่ค้างอยู่ (ถ้ามี) ต่อได้ทันที

Distributed Deadlock

ทำไมซับซ้อนกว่า deadlock บนเครื่องเดียว
แต่ละเครื่องมีแค่ local wait-for graph ของตัวเอง เห็นเฉพาะการรอ lock ที่เกิดขึ้นบนเครื่องนั้น — deadlock ที่เกิดจากวงจรการรอที่ข้ามหลายเครื่องจะไม่ปรากฏในกราฟท้องถิ่นของเครื่องไหนเลย

ต้องมี global deadlock detector รวบรวม local wait-for graph จากทุกเครื่องมาต่อกันเป็นภาพรวม หรือใช้เทคนิค edge chasing — ส่ง probe message ตามทิศทางการรอ ถ้า probe กลับมาหาผู้ส่งเอง แปลว่ามีวงจร (deadlock) จริง

5ทันสมัย: จาก 2PC สู่ Saga Pattern

นี่คือจุดเชื่อมที่สำคัญที่สุดของบทนี้กับภาคปฏิบัติ — microservices แทบไม่ใช้ 2PC กันแล้ว เพราะเหตุผลตรงจากที่อธิบายไปข้างต้น

ประเด็น2PCSaga (Network Computing หัวข้อที่ 4)
ConsistencyStrong consistency — commit พร้อมกันจริงEventual consistency — สำเร็จทีละขั้น มีช่วงที่ข้อมูลยังไม่ตรงกัน
AvailabilityBlocking — service ที่ถือ lock รอ coordinator ต้อง "หยุดรับงานใหม่" ชั่วคราวได้ไม่ block — แต่ละ local transaction commit จริงทันทีในขั้นตอนของตัวเอง ไม่ต้องรอใคร
เมื่อขั้นตอนหนึ่งล้มเหลวRollback ได้จริง (ยังไม่ commit ที่ไหนเลยจนกว่าจะครบ phase 2)ต้องใช้ compensating action "แก้ไขย้อนหลัง" เพราะ step ก่อนหน้า commit จริงไปแล้ว
Coupling ระหว่าง serviceสูง — ทุก participant ต้องพร้อมพร้อมกัน ณ ขณะเดียวกันต่ำกว่า — แต่ละ service ทำงานอิสระ ไม่ต้องรู้จักหรือพึ่งพา availability ของกันโดยตรงตลอดเวลา
ทำไม Saga จึงเหมาะกับ Microservices มากกว่า 2PC
Microservices ออกแบบมาเพื่อให้แต่ละ service deploy และ scale อิสระจากกัน (ดู Network Computing หัวข้อที่ 4) — 2PC ขัดกับเป้าหมายนี้โดยตรง เพราะบังคับให้ทุก service ต้อง "พร้อมพร้อมกัน" ในทุกธุรกรรม ทำให้ availability ของ service หนึ่งผูกติดกับ service อื่นทั้งหมดในธุรกรรมนั้น Saga จึงเป็นทางเลือกที่แลก strong consistency กับ availability และ decoupling — เป็นการตัดสินใจเชิงสถาปัตยกรรมที่สอดคล้องกับ CAP theorem (ดูรายละเอียดเพิ่มเติมในบทที่ 9)
XA Transaction — 2PC ที่ยังอยู่ในโลกจริง
2PC ไม่ได้หายไปทั้งหมด — มาตรฐาน XA (ใช้ใน JMS, database driver จำนวนมาก) ยังใช้ 2PC จริงสำหรับกรณีที่ต้องการ strong consistency ข้ามฐานข้อมูลจำนวนน้อยที่ควบคุมได้เต็มที่ (เช่น ข้าม 2 database ภายในองค์กรเดียวกัน) — ต่างจาก microservices ที่ service อาจเป็นเจ้าของโดยทีมต่างกันและต้อง deploy อิสระ ทำให้ 2PC ไม่เหมาะ
คำถามซ้อมสอบ
  1. อธิบาย Two-Phase Commit Protocol ทีละ phase พร้อมข้อความที่ส่งระหว่าง coordinator กับ participant
  2. อธิบายว่าทำไม participant ต้องเขียน log ก่อนตอบ "Yes" ใน phase 1 เสมอ
  3. อธิบาย blocking problem ของ 2PC และเงื่อนไขที่ทำให้เกิดปัญหานี้
  4. อธิบายว่าทำไม distributed deadlock detection ต้องใช้ global wait-for graph แทนที่จะพึ่ง local wait-for graph ของแต่ละเครื่องอย่างเดียว
  5. เปรียบเทียบ 2PC กับ Saga Pattern ในแง่ consistency, availability และวิธีจัดการเมื่อขั้นตอนหนึ่งล้มเหลว พร้อมอธิบายว่าทำไม microservices จึงเลือกใช้ Saga เป็นหลัก

6จาก Transaction เดียวไปสู่หลาย Participant

ในบทก่อนหน้า database หนึ่งตัวควบคุม lock, log และ commit ได้เอง เมื่อ transaction ข้ามหลายเครื่อง ไม่มีใครแก้ state ของทุก participant ได้ในคำสั่งเดียว แต่ละฝ่ายเห็นเพียง local state และข้อความที่ได้รับ จึงเกิดคำถามใหม่ว่า ทุกฝ่ายจะตัดสินผลเดียวกันได้อย่างไร หากบางฝ่ายพร้อม บางฝ่ายล้ม และ coordinator หายไปกลางทาง

Metaphor: งานแต่งหลายสถานที่
การจัดงานที่ต้องยืนยันโรงแรม ช่างภาพ และสายการบินพร้อมกัน คล้าย distributed transaction ถ้าจ่ายโรงแรมแล้วแต่สายการบินปฏิเสธ เราไม่ได้อยู่ในโลก “ทำทั้งหมดหรือไม่ทำเลย” โดยธรรมชาติ ต้องมีผู้ประสานงาน ขั้นตอนจองชั่วคราว และข้อตกลงว่าจะยืนยันหรือยกเลิกอย่างไร ที่สำคัญ การไม่รับโทรศัพท์ไม่ได้บอกว่าฝ่ายนั้นปฏิเสธ อาจเพียงติดต่อไม่ได้
คำศัพท์หน้าที่สิ่งที่ต้องเก็บถาวร
Coordinatorเริ่ม protocol รวบรวม vote และประกาศ decisiontransaction ID, participant, global decision
Participant/Cohortทำ local work, prepare และทำตาม decisionprepared state, locks/resources, final outcome
Transaction managerจัด lifecycle ของ global transactionlog ที่ใช้ recovery
Resource managerจัด database/queue/resource ใน local transactionundo/redo information

Atomicity ข้ามเครื่องไม่ได้เกิดจากการเรียก API ต่อกัน

การเขียนโค้ด debit(); credit(); ทำให้ลำดับดูชัด แต่ถ้า process crash ระหว่างสอง call เงินถูกหักโดยยังไม่เพิ่มปลายทาง การ catch exception แล้วเรียก refund ช่วยบางกรณี แต่ refund call ก็ล้มได้ และ caller อาจไม่รู้ debit สำเร็จหรือไม่เพราะ response หาย

Distributed transaction จึงต้องนิยาม state machine และ recovery behavior ไม่ใช่เพียง happy path ทุก transition ที่มีผลต่อ decision ต้องบันทึกลง stable storage ก่อนส่งข้อความบางชนิด เพื่อให้ process หลัง restart รู้ว่าตนสัญญาอะไรไว้แล้ว

7Two-Phase Commit แบบอ่านจาก State Machine

2PC แบ่งช่วงเป็น vote/prepare และ decision/commit การแบ่งสองช่วงไม่ใช่พิธีกรรม แต่แก้ความต่างระหว่าง “ฉันทำ local transaction ได้” กับ “ทุกคนต้องทำผลเดียวกัน” Participant ที่ vote YES ต้องเตรียม local state และสัญญาว่าจะ commit ได้ภายหลัง แม้ restart แล้วก็ตาม

Coordinator                         Participants
    |--- PREPARE(tx) ------------------>| local validate + force PREPARED log
    |<-- YES / NO ----------------------|
    | decide COMMIT iff every vote YES
    | force GLOBAL-COMMIT log
    |--- COMMIT(tx) ------------------->| local commit + release locks
    |<-- ACK ---------------------------|
จุดสิ่งที่ต้องเป็นจริงก่อนเดินต่อเหตุผล
Participant vote YESlocal work valid และ prepared state durableห้ามกลับใจหลัง coordinator ตัดสิน commit
Coordinator ประกาศ COMMITได้รับ YES ครบและ decision ลง logrestart แล้วต้องประกาศผลเดิม
Participant รับ decisionทำ local outcome ตาม decision และ logปล่อย lock/resource อย่างปลอดภัย
Coordinator ลบ stateได้รับ ACK ตามนโยบายมั่นใจว่าทุกฝ่ายเรียนรู้ผลแล้ว

จุดที่ไม่มีทางกลับของ Participant

ก่อน vote YES participant ยัง abort ได้เอง แต่หลัง YES มันอยู่ใน uncertain/prepared state และห้ามตัดสินใจเอง หาก abort ขณะที่ global decision เป็น commit จะละเมิด atomicity ข้อแลกเปลี่ยนคือมันอาจต้องถือ lock และ resource รอ coordinator นานมาก

นี่คือหัวใจของ blocking 2PC ไม่ใช่เพราะ implementation ขาด timeout การใส่ timeout แล้ว participant abort เองหลัง vote YES ทำให้ไม่ block แต่ทำลาย atomic guarantee Timeout บอกว่า decision ยังไม่มาถึง ไม่ได้ให้ข้อมูลว่า decision เป็นอะไร

8Failure Matrix: ล้มตรงไหนแล้วใครรู้อะไร

FailureState ที่อาจค้างการกู้คืนความกำกวม
Participant ล้มก่อน votecoordinator ยังไม่มี YEStimeout แล้ว global abortไม่มี commitment จาก participant
Participant ล้มหลัง YESprepared durablerestart แล้วถาม coordinator/participant อื่นห้ามตัดสินเองจนรู้ decision
Coordinator ล้มก่อน decisionบาง participant preparedอ่าน log; ถ้าไม่มี decision อาจ abort ตาม protocolprepared participant อาจ block
Coordinator ล้มหลัง log COMMIT ก่อนส่งglobal decision คือ commitrestart แล้ว resend COMMITparticipant บางตัวรู้ บางตัวไม่รู้
COMMIT/ACK หายผลอาจเสร็จแล้วส่ง decision ซ้ำ; operation ต้อง idempotentcoordinator ไม่รู้ว่า participant เรียนรู้ผลหรือยัง

ตารางนี้แสดงหลักคิดสำคัญ: recovery decision มาจาก durable log และ state machine ไม่ได้มาจากการเดาจากเวลาที่ผ่านไป การส่ง COMMIT ซ้ำต้องปลอดภัย Participant ที่ commit แล้วรับ COMMIT เดิมควรตอบ outcome เดิม ไม่ commit business effect ซ้ำ

presumed abort และ presumed commit

Optimization บางแบบลด log/message โดยกำหนด default เมื่อไม่มี record Presumed abort ถือว่า transaction ที่ไม่มี commit evidence เป็น abort เหมาะเมื่อ abort บ่อยหรือต้องการลด abort logging Presumed commit ทำตรงข้ามและต้องจัด metadata ต่างกัน จุดสำคัญคือ “ไม่มีข้อมูล” มีความหมายได้ต่อเมื่อ protocol กำหนดไว้ ไม่ใช่สรุปเอง

Heuristic Decision

ระบบบางชนิดยอมให้ operator/participant ตัดสิน commit หรือ rollback เองเมื่อ block นานมาก เรียกว่า heuristic decision ซึ่งคืน availability แต่เสี่ยง global inconsistency จึงต้องบันทึก audit, แจ้งเตือน และมีกระบวนการ reconcile ไม่ควรซ่อนว่าเป็น success ปกติ

9Nested Transaction และ Open Nesting

Nested transaction แบ่งงานใหญ่เป็น subtransaction โครงต้นไม้ ลูกอาจ commit provisional ต่อ parent แต่ผลสุดท้ายยังขึ้นกับ root ถ้า parent abort ผลลูกต้องถูกยกเลิก โมเดลนี้ช่วย parallelism และแยก failure แต่ไม่ได้หมายความว่าลูกที่ commit แล้วเปิดเผยต่อโลกเสมอ

แบบการเห็นผลของลูกเมื่อ parent abort
Closed nestingผลลูกเห็นภายใน parent ยังไม่เปิดสู่ภายนอกrollback ตาม atomic transaction
Open nestingลูก commit และเปิดผลก่อน root จบใช้ compensating action แทน physical rollback
Flat transactionมีขอบเขตเดียวrollback ทั้งหมด

Open nesting ใกล้โลกธุรกิจมากกว่า เช่นจองบริการภายนอกแล้วต้องยกเลิกภายหลัง แต่ compensation ไม่ใช่ inverse ที่สมบูรณ์เสมอ การส่งอีเมลย้อนกลับไม่ได้ เราทำได้เพียงส่งอีเมลแก้ไข การคืนเงินอาจใช้เวลาและมีค่าธรรมเนียม ดังนั้น state หลัง compensation อาจ business-equivalent แต่ไม่เหมือน state ก่อนเริ่มทุก byte

102PC ไม่เท่ากับ Consensus

2PC ทำ atomic commitment โดยมี coordinator และ participant vote ถ้า coordinator หายหลังทุกคน prepare ระบบอาจ block ส่วน consensus ให้สมาชิกตกลงค่าเดียวกันภายใต้ failure model และมักเดินหน้าต่อได้เมื่อมี quorum แม้สมาชิกบางส่วนล้ม ทั้งสองมีขั้นตอนส่งข้อความคล้ายกันบางส่วน แต่ตอบโจทย์คนละอย่าง

มิติ2PCConsensus/Raft-style
เป้าหมายทุก participant commit/abort transaction เดียวกันreplica ตกลงลำดับ/value เดียวกัน
การตัดสิน commitต้อง YES จาก participant ทุกคนโดยทั่วไป quorum
Coordinator/leader ล้มprepared participant อาจ blockเลือก leader ใหม่เมื่อ quorum ติดต่อได้
การแทนกันparticipant แต่ละคนถือ resource คนละส่วนreplica มักเก็บ state machine ซ้ำกัน

ฐานข้อมูลสมัยใหม่อาจใช้ consensus ภายในแต่ละ shard เพื่อ replicate log แล้วใช้ 2PC ข้าม shard ดังนั้น “ใช้ Raft แล้วไม่ต้องมี distributed transaction” ไม่จริงโดยอัตโนมัติ Consensus ช่วยให้ coordinator/participant state ทน fault แต่ cross-resource atomicity ยังต้อง protocol เพิ่ม

11ข้อจำกัดเชิงปฏิบัติของ 2PC

นอกจาก blocking แล้ว 2PC เพิ่มรอบ network, force log, lock duration และ tail latency Participant ที่ช้าที่สุดกำหนดเวลาทั้ง transaction และ failure probability สะสมตามจำนวน participant จึงไม่เหมาะกับ workflow ข้าม service จำนวนมากหรือ external service ที่ไม่รองรับ prepare

ภาพต้นทุนอย่างง่าย
Latency ≈ local work + prepare round + decision round + durable log waits
เมื่อกระจายข้าม region รอบสื่อสารมีราคาสูง และ lock ถูกถือครอบเวลานี้ทั้งหมด

แต่ไม่ควรสรุปว่า 2PC “ล้าสมัย” ใน database cluster ที่ควบคุม participant, transaction สั้น และต้อง strong atomicity มันยังเหมาะสม สิ่งที่ไม่เหมาะคือยืด 2PC ข้าม business service และคนละองค์กรโดยไม่รับต้นทุน availability/coupling

12Saga: เปลี่ยน Atomicity เป็นกระบวนการที่มองเห็นได้

Saga แบ่ง long-lived transaction เป็น local transaction หลายขั้น แต่ละขั้น commit จริงและอาจเผยผล จากนั้นเรียกขั้นถัดไป หากภายหลังล้ม ใช้ compensating transaction ย้อนผลทางธุรกิจเท่าที่ทำได้ Saga จึงไม่ให้ isolation/atomicity แบบ ACID ทั้งก้อน แต่ให้วิธีควบคุม workflow ที่มี intermediate state

ขั้นForward actionCompensationสิ่งที่ย้อนสมบูรณ์ไม่ได้
1สร้าง order PENDINGเปลี่ยนเป็น CANCELLEDเลข order/audit ยังคงอยู่
2กันวงเงินrelease/refundเวลาคืนและค่าธรรมเนียม
3จอง stockคืน stockลูกค้าอื่นอาจเห็น stock ลดชั่วคราว
4ส่ง notificationส่งข้อความแก้ไขผู้ใช้เห็นข้อความแรกแล้ว

Backward recovery กับ Forward recovery

Backward recovery ชดเชยกลับเมื่อขั้นล้ม Forward recovery retry/ซ่อมให้ workflow เดินต่อจนสำเร็จ งานบางชนิดควร forward เช่นบันทึกบัญชีที่ commit แล้วแต่ notification ล้ม ส่วนบางชนิดควร compensate เช่นกัน stock แล้ว payment ปฏิเสธ ต้องกำหนด pivot transaction ซึ่งหลังผ่านแล้วเลือกเดินหน้ามากกว่าย้อน

Choreography

แต่ละ service ฟัง event แล้วทำ local action/ปล่อย event ถัดไป ข้อดีคือลด coordinator กลางและเพิ่ม autonomy แต่ workflow กระจายอยู่หลายที่ ตามลำดับยาก เกิดวง event และเปลี่ยน process ยากเมื่อขั้นเพิ่ม ควรมี event contract, correlation ID และแผนภาพ workflow ที่เป็นเจ้าของร่วม

Orchestration

Orchestrator เก็บ state machine ส่ง command และรับผล ทำให้เห็น flow, timeout และ compensation ชัด แต่ orchestrator ต้องทน fault และไม่ควรกลายเป็นที่เก็บ business logic ทุก domain มันประสาน “ใครทำต่อ” ส่วน service ยังตัดสินกฎภายในตน

คำถามChoreographyOrchestration
เห็นสถานะ workflowต้องประกอบจาก event หลายแหล่งมี state กลางชัด
couplingผูกผ่าน event/schema และ implicit flowผูก command กับ orchestrator
เปลี่ยนลำดับกระทบหลาย consumerเปลี่ยน state machine ง่ายกว่า
failure ของตัวควบคุมไม่มีตัวเดียว แต่เกิด emergent behaviorต้อง replicate/recover orchestrator

13Dual Write Problem และ Transactional Outbox

Service ต้องเขียน database แล้ว publish event ถ้าทำสองคำสั่งแยกกัน ย่อมมีช่อง crash: เขียน DB สำเร็จแต่ publish ไม่สำเร็จ ทำให้โลกภายนอกไม่รู้ หรือ publish ก่อนแล้ว DB rollback ทำให้ event บอกสิ่งที่ไม่เกิดจริง นี่คือ dual write problem

Outbox เขียน business row และ outbox event ใน local transaction เดียว จากนั้น relay อ่าน outbox ไปส่ง broker เพราะสอง record commit/abort พร้อมกัน เราจึงไม่เสีย event ระหว่าง local commit แต่ relay อาจส่งซ้ำหาก crash หลัง publish ก่อน mark sent Consumer ยังต้อง idempotent

BEGIN;
  INSERT INTO orders(...);
  INSERT INTO outbox(event_id, type, payload, status) VALUES (...);
COMMIT;

Relay: read unsent → publish → mark sent   # publish อาจเกิดซ้ำ

CDC เป็นอีกเส้นทาง

Change Data Capture อ่าน database log แล้วแปลง change เป็น stream ลด polling และอาจรักษาลำดับตาม log แต่ raw table change ไม่เท่ากับ domain event และ schema ภายในรั่วได้ Outbox + CDC ใช้ร่วมกันได้ โดย application เขียน domain event ลง outbox และ CDC ส่งออก

14Idempotency, Inbox และการรับ Event ซ้ำ

At-least-once delivery เป็นทางเลือกทั่วไปเพราะยอมส่งซ้ำดีกว่าทำหาย Consumer ใช้ Inbox/Dedup table เก็บ event ID ใน transaction เดียวกับ side effect ถ้า ID เคยถูกใช้ คืนผลเดิม/ack โดยไม่ทำซ้ำ ถ้าเก็บ ID หลัง side effect แยกคำสั่ง ยังมีช่อง crash

ขอบเขตคำถาม
Key scopeID ไม่ซ้ำทั้งระบบ ต่อ producer หรือ operation
Retentionต้องจำ duplicate นานเท่า retry/replay window ใด
Atomicitydedupe record กับ business effect commit พร้อมกันหรือไม่
Resultduplicate ต้องได้ผลเดิมหรือเพียงไม่ทำซ้ำ
Concurrencyสอง delivery ID เดียวกันเข้าพร้อมกันใช้ unique constraint หรือไม่

15Isolation ใน Saga และสิ่งที่ผู้ใช้อาจเห็น

เพราะ local transaction commit ทีละขั้น service อื่นอาจเห็น intermediate state เช่น order PENDING, เงิน HELD และ stock RESERVED พร้อมกัน ปัญหาได้แก่ dirty read เชิงธุรกิจ, lost update และ semantic lock จึงใช้ state machine, reservation, version และ policy ว่า operation ใดทำได้ในแต่ละ state

ตัวอย่างเช่นห้ามแก้รายการสินค้าหลัง payment hold หรือใช้ countermeasure แบบ reread ก่อน commit Compensation ต้องตรวจ version ไม่ย้อน state ที่ถูก workflow ใหม่เปลี่ยนไปแล้ว การชดเชยแบบ “ตั้ง stock กลับค่าเดิม” อาจทับ order อื่น ควรใช้ operation เพิ่ม/ลดเชิงสัมพันธ์พร้อม identity

16เลือก 2PC, Saga หรือออกแบบขอบเขตใหม่

สถานการณ์แนวทางที่ควรพิจารณาเหตุผล
หลาย table ใน DB เดียวLocal ACID transactionง่ายและ guarantee แข็ง ไม่ควรรีบกระจาย
หลาย shard/resource manager ที่ควบคุมได้2PC/DB distributed transactionต้อง atomicity และ transaction สั้น
business workflow ยาว ข้าม serviceSaga + explicit stateไม่ถือ lock ยาว ยอม intermediate state
เขียน DB + publish eventOutbox/CDCแก้ dual write ด้วย local atomicity
invariant ถูกแยกข้าม serviceบ่อยทบทวน service boundaryอาจแบ่ง ownership ผิด ไม่ควรแก้ทุกอย่างด้วย protocol

การออกแบบที่ดีที่สุดบางครั้งไม่ใช่เลือก protocol เก่งขึ้น แต่ย้ายข้อมูลที่ต้อง atomic มาอยู่ ownership เดียวกัน ถ้าสอง service ต้อง lock/commit พร้อมกันแทบทุก operation ความเป็นอิสระอาจเป็นเพียงภาพบนสไลด์

17Workshop: ทำ Failure ให้ครบทุกช่อง

ส่วน A — 2PC State Machine

  1. จำลอง coordinator กับ participant สองตัว แต่ละตัวมี durable log
  2. หยุด coordinator ก่อน decision, หลัง log decision และระหว่างส่ง decision
  3. restart แล้วพิสูจน์ว่าประกาศ outcome เดิม
  4. ทำ participant crash หลัง YES และแสดงว่ามันตัดสินเองไม่ได้
  5. วัดเวลาถือ lockและอธิบาย blocking ไม่แก้ด้วย timeout อย่างไร

ส่วน B — Saga Order

  1. ออกแบบ state machine และ compensation ทุกขั้น
  2. ทำ failure หลัง local commit แต่ก่อน event publish แล้วเพิ่ม Outbox
  3. ทำ relay ส่งซ้ำและเพิ่ม Inbox/idempotent consumer
  4. ส่ง event ผิดลำดับและกำหนด version/state guard
  5. สร้างหน้าตรวจ workflow ที่ค้างและ manual resolution
รายงานต้องตอบ
แต่ละจุด crash ใครรู้ global decision บ้าง ข้อมูลใด durable งานใดอาจซ้ำ intermediate state ใดผู้ใช้เห็น และการ recovery เลือกย้อนกลับหรือเดินหน้าด้วยเหตุผลอะไร

18ทบทวนแกนสำคัญก่อนกรณีศึกษา

Distributed transaction ยากเพราะ local commit หลายตัวต้องกลายเป็น outcome เดียวทั้งที่สมาชิกและเครือข่ายล้มแยกกันได้ 2PC รักษา atomic commitment ด้วย prepared promise และ durable log แต่จ่ายด้วย blocking, รอบสื่อสาร และการถือ resource นาน

Saga ไม่ใช่ 2PC รุ่นเร็วกว่า มันเปลี่ยน guarantee: local step commit และเปิดเผย intermediate state แล้วใช้ compensation/forward recovery ทำให้ workflow ไปถึงสถานะปลายทาง Outbox แก้ช่อง dual write แต่ยังส่งซ้ำได้ จึงต้องจับคู่กับ idempotent consumer และ observability

ขั้นตอนถัดไปคือ Name Services เพราะระบบที่มี service, participant และ replica เปลี่ยนตำแหน่งตลอดต้องแยก “สิ่งนี้คือใคร” ออกจาก “ตอนนี้ติดต่อที่ไหน” Naming ทำให้ location เปลี่ยนได้โดย reference เชิงตรรกะยังอยู่ แต่ cache, TTL และ stale mapping จะสร้าง distributed consistency problem ชุดใหม่

19กรณีศึกษา: การโอนเงินที่คำตอบหาย

Client ขอให้ Transfer Service โอน 1,000 บาทจาก A ไป B Service สร้าง transaction ID T100 แล้วเรียก Ledger-A และ Ledger-B สมมุติว่า debit และ credit อยู่คนละ participant ทั้งคู่ vote YES Coordinator บันทึก COMMIT แต่ crash ก่อน B ได้ decision Client เห็น timeout และไม่รู้ผล

จุดนี้มีความจริงหลายระดับ Coordinator หลัง restart อ่าน log แล้วรู้ว่า global decision คือ COMMIT; A อาจ commit แล้ว; B prepared และรอ; Client ไม่รู้อะไรเกิน timeout ถ้า client สร้าง transaction ID ใหม่แล้ว retry ระบบอาจโอนอีกครั้ง แม้ 2PC ชุดแรกทำงานถูกต้อง จึงต้องมี idempotency ที่ boundary ของ business request นอกเหนือจาก atomicity ภายใน 2PC

ฝ่ายState หลัง crashการกระทำที่ถูกสิ่งที่ห้ามเดา
Clientdeadline exceededquery ด้วย T100 หรือ retry ID เดิมtimeout = abort
CoordinatorCOMMIT durableresend decision จน ACKสร้าง decision ใหม่
Acommittedตอบ outcome เดิมเมื่อรับซ้ำdebit ซ้ำ
Bpreparedรอ/ถาม decision แล้ว commitabort เองเพราะรอนาน

ถ้า Coordinator ไม่มี decision log และ crash ก่อนตัดสิน มันอาจเลือก abort ตาม presumed-abort rule แต่ถ้า log COMMIT แล้ว outcome เปลี่ยนไม่ได้ Durable log จึงไม่ได้มีไว้แค่ audit แต่เป็น memory ที่ทำให้ process incarnation ใหม่รักษาคำสัญญาของ incarnation เก่า

คำถามที่ควรใช้ตรวจการออกแบบ

  1. API ให้ผู้ใช้สอบถาม outcome ด้วย transaction ID ได้หรือไม่
  2. idempotency record กับการเริ่ม global transaction ผูกกันอย่างไร
  3. ถ้า participant prepared นาน มี alert และ manual resolution อย่างไร
  4. log เก็บนานเท่า retry/recovery window และลบเมื่อใด
  5. operator เห็นคำว่า UNKNOWN แยกจาก FAILED หรือไม่

20กรณีศึกษา: Order Saga ที่ย้อนกลับไม่หมด

Workflow รับ order ประกอบด้วย CreateOrder, AuthorizePayment, ReserveInventory, ArrangeDelivery และ NotifyCustomer ถ้า ArrangeDelivery ล้มหลังร้านเริ่มเตรียมสินค้า การคืน stock กับ release payment อาจทำได้ แต่เวลาของร้านและข้อความที่ส่งไปแล้วหายไม่ได้ Compensation จึงต้องสะท้อนนโยบายธุรกิจ เช่นคืนเงินพร้อมเครดิต ไม่ใช่แกล้งทำว่าเหตุการณ์ไม่เคยเกิด

Stateเหตุการณ์ที่พาเข้าสิ่งที่อนุญาตทางออกเมื่อหมดเวลา
PENDING_PAYMENTOrderCreatedauthorize/cancelcancel draft
PAYMENT_HELDPaymentAuthorizedreserve/releaserelease hold
STOCK_RESERVEDInventoryReservedarrange deliveryrelease + refund policy
CONFIRMEDDeliveryArrangedfulfil/cancel policycompensation อาจมีค่าปรับ
MANUAL_REVIEWcompensation failedoperator actionห้าม retry แบบไร้เพดาน

State machine มีความสำคัญเพราะ event อาจมาช้าและซ้ำ เช่น PaymentAuthorized มาถึงหลัง order ถูก cancel Consumer ต้องตรวจ current state/version ไม่ใช่ทำตาม event ทุกฉบับแบบไร้บริบท หาก authorization เกิดจริงหลัง cancel อาจต้องส่ง ReleasePayment ใหม่แทนการเพิกเฉย

Poison Workflow

Workflow บางรายการไม่สามารถเดินหน้าหรือชดเชยได้เพราะข้อมูลผิด/บริการภายนอกปฏิเสธถาวร การ retry ต่อเนื่องสร้าง noise และค่าใช้จ่าย ต้องมี retry class, max attempt, dead-letter/work queue สำหรับคน และข้อมูลที่ช่วยตัดสินใจ การมี manual state ไม่ใช่ความพ่ายแพ้ แต่เป็นการยอมรับว่าธุรกิจบางกรณีไม่มี inverse อัตโนมัติที่ปลอดภัย

21ความสัมพันธ์ระหว่าง Transaction, Messaging และ Audit

ฐานข้อมูลตอบ state ปัจจุบัน Event log ตอบว่าเดินมาถึง state นี้อย่างไร Audit log ตอบว่าใครทำอะไรภายใต้สิทธิ์ใด ทั้งสามอาจเชื่อมกันแต่ไม่ควรใช้แทนกันโดยไม่กำหนด semantics Outbox event ที่สร้างเพื่อ integration อาจไม่มีรายละเอียดพอเป็น regulatory audit และ application log อาจถูกลบ/แก้ได้

ข้อมูลวัตถุประสงค์ข้อกำหนด
Business stateให้บริการปัจจุบันinvariant, query, update concurrency
Outbox/integration eventแจ้ง service อื่นschema, delivery, idempotency
Workflow historyrecover state machinedeterministic transition, retention
Audit recordตรวจสอบผู้กระทำ/เหตุผลtamper evidence, identity, policy

Event Sourcing กับ Saga

Event-sourced aggregate บันทึก event เป็น source of truth แล้ว fold เป็น state ช่วย audit/rebuild และ correlation กับ Saga แต่ไม่ได้แก้ cross-aggregate transaction ฟรี Event append ต้อง concurrency control, projection lag และ schema evolution Saga ยังต้องประสาน local transaction หลาย aggregate/service และ event อาจส่งซ้ำ

22แบบฝึกวิเคราะห์ Failure ทีละบรรทัด

สำหรับแต่ละเหตุการณ์ต่อไปนี้ ให้ตอบห้าข้อ: local state คืออะไร, durable record ใดมีอยู่, message ใดอาจอยู่ระหว่างทาง, operation ใดทำซ้ำได้ และ safety/liveness ใดเสี่ยง

เหตุการณ์ประเด็นที่ต้องค้นพบ
Coordinator ส่ง PREPARE ให้ P1 แต่ยังไม่ถึง P2 แล้ว crashP1 อาจ vote/prepare ส่วน P2 ไม่รู้ transaction; ไม่มี global commit
P1 vote YES แต่ vote reply หายCoordinator timeout abort ได้ แต่ P1 ต้องเรียนรู้ ABORT เพื่อปล่อย lock
ทุกคน YES, coordinator log COMMIT แล้ว disk หายถาวรsingle durable coordinator เป็น failure domain; ต้อง backup/replicate log
Saga publish PaymentAuthorized ซ้ำconsumer dedupe และ state guard; event delivery ≠ business effect
Compensation Refund timeoutไม่รู้ refund เกิด; query/retry ID เดิม ไม่ออก refund ใหม่
Outbox relay publish แล้ว crashrow ยัง unsent จึง publish ซ้ำ; consumer inbox จำเป็น
Event รุ่นใหม่ถึง consumer เก่าcompatibility/unknown field; retry ไม่แก้ schema ถาวร
Operator สั่ง heuristic commit ฝ่ายหนึ่งต้อง reconcile global state และบันทึก hazard

คำถามเชิงออกแบบเพิ่มเติม

  1. ทำไม 2PC ต้องบังคับ participant เก็บ PREPARED ก่อน vote YES
  2. เหตุใด timeout หลัง YES ไม่อนุญาตให้ participant abort เอง
  3. อะไรทำให้ Saga มี availability สูงกว่า และ guarantee ใดที่เสียไป
  4. compensation ต่างจาก database rollback อย่างไรเมื่อคนภายนอกเห็นผลแล้ว
  5. Outbox แก้ event loss แต่เหตุใดไม่ให้ exactly-once consumer effect อัตโนมัติ
  6. เมื่อไรปัญหา distributed transaction เป็นสัญญาณว่า service boundary ผิด
  7. ถ้าค่าใช้จ่าย duplicate สูงมาก จะวาง idempotency ที่ boundary ใดบ้าง
  8. จะพิสูจน์ว่า workflow ที่ค้างถูกค้นพบ “ในที่สุด” ด้วย metric/scan ใด

23Checklist ก่อนนำ Workflow ขึ้นใช้งานจริง

24คำศัพท์ที่ควรอธิบายเป็นประโยค ไม่ใช่จำเพียงชื่อ

คำคำอธิบายที่ใช้วิเคราะห์ระบบได้
Atomic commitmentการทำให้ participant ทุกคนได้ outcome commit/abort เดียวกัน ไม่ใช่การทำให้ทุกขั้นเกิดพร้อมกันทางกายภาพ
Preparedparticipant ทำ local work และบันทึกคำสัญญาถาวรว่าจะ commit ได้หาก global decision สั่ง จึงถอนตัวเองไม่ได้
Blockingสถานะที่ participant ไม่มีข้อมูลพอตัดสินโดยไม่เสี่ยงละเมิด atomicity ไม่ใช่เพียง thread รอนานจากโค้ดไม่ดี
Compensationธุรกรรมใหม่ที่ลด/แก้ผลทางธุรกิจของสิ่งที่ commit แล้ว ไม่ใช่ physical undo ที่ทำให้ประวัติหาย
Pivot transactionจุดใน Saga ที่หลังสำเร็จแล้ว workflow มักต้องเดินหน้าให้จบ มากกว่าย้อนทุกขั้นกลับ
Semantic lockสถานะ/เครื่องหมายระดับธุรกิจที่จำกัด operation ระหว่าง Saga เช่น RESERVED ไม่ใช่ DB lock ที่ถือ connection ยาว
Reconciliationการตรวจและซ่อม divergence จากหลักฐานหลายระบบภายหลัง เป็น safety net ไม่ควรแทน protocol หลักทั้งหมด
In-doubtรู้ว่ามี transaction แต่ยังไม่รู้ final decision ต้องรักษาข้อมูลและห้ามรายงาน FAILED แบบมั่นใจเกินจริง

ประโยคที่ควรหลีกเลี่ยง

แบบฝึกตัดสินใจสั้น ๆ

ระบบมหาวิทยาลัยต้องลงทะเบียนรายวิชา ตัดที่นั่ง และบันทึกค่าธรรมเนียม หากทั้งหมดอยู่ฐานเดียว ควรเริ่ม local transaction หากแยก service ให้เขียน invariant ก่อนว่า “ได้ที่นั่งแต่ยังไม่จ่าย” ยอมเป็นสถานะชั่วคราวได้หรือไม่ ถ้าได้ ออกแบบ reservation expiry/Saga ถ้าไม่ได้และ participant ควบคุมร่วม อาจพิจารณา distributed transaction หรือรวม ownership ใหม่

การเลือกคำตอบต้องบอก failure behavior ด้วย: นักศึกษาเห็น PENDING นานเท่าใด ที่นั่งคืนเมื่อใด payment response หายสอบถามอย่างไร และ reconciliation ตรวจที่นั่งกับใบเสร็จแบบใด คำว่าใช้ microservices หรือ queue ยังไม่ตอบคำถามเหล่านี้

25Design Review: คำถามที่เปิดช่องโหว่ได้เร็วกว่าดูแผนภาพ

เวลา review ระบบ อย่าเริ่มจากถามว่าใช้ Kafka หรือ workflow engine อะไร ให้เดินตามหนึ่ง business ID แล้วหยุดหลังทุก state change: ถ้า process ตายตรงนี้ state ใด commit แล้ว ข้อความใดอาจส่งแล้ว ใครจะตรวจพบ และการทำซ้ำสร้างผลอะไร วิธีนี้เรียบง่ายแต่เปิดช่อง dual write, missing timeout และ compensation ที่ไม่ idempotent ได้เร็วมาก

จุดตรวจคำถามบังคับ
ก่อน local commitabort ได้ครบหรือมี external effect เกิดแล้ว
หลัง commit ก่อน publishOutbox/CDC รับประกัน event ไม่หายหรือไม่
หลัง publish ก่อน markส่งซ้ำแล้ว consumer ปลอดภัยหรือไม่
หลัง consumer effect ก่อน ACKInbox/effect atomic หรือ duplicate เกิด
ระหว่าง compensationforward workflow ใหม่เปลี่ยน state ไปแล้วหรือไม่
เมื่อหมด deadlineรู้ outcome หรือเพียงหยุดรอ และมี query statusไหม

ให้ตรวจ cardinality ด้วย Request หนึ่งอาจ fan-out หลาย participant แต่ retry ซ้อนชั้นทำให้จำนวน call โตแบบคูณ ควรมี retry budget, deadline และ metric attempts ต่อ business operation ไม่ใช่นับ HTTP request อย่างเดียว มิฉะนั้น dashboard อาจบอก throughputสูงขึ้นในวันที่ระบบเพียงทำงานเดิมซ้ำมากขึ้น

สุดท้ายให้ถามว่า invariant ใดถูกตรวจภายใน ownership เดียวและ invariant ใดอาศัย reconciliation หากคำตอบคือทุกอย่างตรวจภายหลัง ระบบอาจเปิด inconsistency มากเกินไป แต่ถ้าทุกอย่างต้อง global lock ระบบอาจไม่มี availability พอ การออกแบบคือการเลือกเส้นแบ่ง ไม่ใช่หลบการเลือกด้วยศัพท์ pattern

เกณฑ์ว่าระบบพร้อมหรือยัง

ระบบไม่ควรถูกเรียกว่าพร้อมเพียงเพราะ happy path จบครบ ต้องผ่านการทดสอบ kill หลังทุก commit/send, replay message, restart ด้วย log เดิม และตรวจ invariant หลังงานจำนวนมาก มี alert สำหรับ prepared/in-doubt ที่อายุเกินเกณฑ์ Saga ที่ค้าง compensation ที่ล้ม และ outbox/inbox ที่โตผิดปกติ

การทดสอบควรสุ่ม delay, duplicate, reorder และ dependency unavailable โดยรักษา business ID ให้ตามเรื่องได้ หลังจบให้ reconciliation เปรียบเทียบยอด/จำนวนข้าม service หากผลรวมถูกแต่มีรายการค้าง manual จำนวนมาก ระบบยังไม่พร้อมด้านปฏิบัติการ เพราะ correctness ที่ต้องพึ่งคนแก้ทุกคืนเป็น correctness ที่มีต้นทุนจริง

สุดท้ายต้องมีภาษาสถานะสำหรับผู้ใช้ ไม่รายงาน FAILED เมื่อ outcome ยัง UNKNOWN และไม่รายงาน SUCCESS หากยังมีขั้นสำคัญ pending หน้าจอที่ยอมแสดง “กำลังตรวจสอบผล” อย่างซื่อสัตย์มักปลอดภัยกว่าการบังคับทุกความไม่แน่นอนให้กลายเป็นสำเร็จหรือล้มเหลวทันที

อีกด้านหนึ่ง การเก็บ metric ต้องนับตาม business operation ไม่ใช่ message อย่างเดียว เช่นหนึ่งการชำระเงินมี delivery ห้าครั้งแต่ effect หนึ่งครั้ง ระบบ messaging อาจรายงาน throughput ห้ารายการ ขณะที่ธุรกิจมีงานเดียว หากไม่แยก attempt, delivery และ effect เราจะประเมิน capacity กับ error rate ผิด รวมถึงมอง retry storm เป็นปริมาณงานที่เติบโต ทั้งที่เป็นงานเดิมหมุนซ้ำ

สำหรับงานที่มีข้อกำกับ ควรเชื่อม transaction ID, user identity, policy version และเหตุผล manual override ไว้ใน audit ที่แก้ไขย้อนหลังไม่ได้ง่าย แต่ต้องระวังไม่เก็บ secret หรือข้อมูลส่วนตัวเกินจำเป็น การ trace ได้ไม่ได้แปลว่าต้องสำเนา payload ทุกชิ้นตลอดกาล Retention และการปกปิดข้อมูลเป็นส่วนหนึ่งของ design เช่นเดียวกับ commit protocol

คำถามสุดท้ายในการ review คือ “ถ้าทีมเจ้าของ service นี้หายไปหนึ่งวัน ระบบยังอธิบายและฟื้นงานค้างได้หรือไม่” ถ้าคำตอบพึ่งคนจำลำดับ call จากประสบการณ์ แสดงว่า state machine, observability หรือ runbook ยังไม่ครบ ระบบกระจายที่ดีต้องฝากความจำไว้ใน durable state และหลักฐาน ไม่ฝากไว้ในความจำของคนเวรเพียงคนเดียว

26สรุปและขั้นตอนถัดไป

บทนี้เริ่มจากความต้องการที่ดูเรียบง่ายว่า participant หลายตัวควร commit หรือ abort ให้ตรงกัน แล้วพบว่าความยากอยู่ที่แต่ละฝ่ายรู้ decision ไม่พร้อมกัน 2PC ใช้ prepared promise และ durable log รักษา atomic outcome แต่หลัง vote YES participant อาจต้อง block เพราะ timeout ไม่เปิดเผย decision

Saga เลือกอีกขอบเขตหนึ่ง: ให้ local transaction commit ทีละขั้น เปิด intermediate state และใช้ compensation หรือ forward recovery Outbox/Inbox ช่วยเชื่อม database กับ messaging โดยยอม duplicate และทำ effect ให้ idempotent สิ่งเหล่านี้ไม่ใช่การสร้าง exactly-once จากอากาศ แต่เป็นการจัดวางหลักฐานและ atomic boundary ให้ความซ้ำไม่ทำลาย invariant

ขั้นตอนถัดไปคือ Name Services เราจะเปลี่ยนจากคำถามว่า “ทุกฝ่ายตกลงผลอย่างไร” ไปสู่ “ทุกฝ่ายรู้ได้อย่างไรว่ากำลังอ้างถึง entity เดียวกัน” เมื่อ service และ replica ย้ายตำแหน่ง Naming จะคั่น identity ออกจาก address แต่ cache, TTL และ authority จะนำ consistency problem รูปแบบใหม่เข้ามา