“Gói mọi thứ trong một transaction” là mô hình tâm lý dễ hiểu khi bạn chỉ có một database. Khi có queue, partner API, bucket object, hoặc database thứ hai, ranh giới ACID kết thúc — và bạn phải thiết kế lại boundary, retry, và điều kiện thất bại.
Phạm vi: pattern vận hành; không so sánh 2PC vs Paxos chi tiết. Tham chiếu: Quản lý transaction & concurrency, CDC & Outbox.
1. Local transaction: khi nào “đủ”
Đủ khi mọi invariant nghiệp vụ có thể kiểm tra và commit trong cùng một DB transaction:
- Chuyển tiền giữa hai tài khoản cùng shard (hoặc cùng DB) với row lock đúng thứ tự.
- Đặt hàng + trừ kho cùng schema có constraint.
Không đủ khi có side-effect bên ngoài DB trong cùng “đơn vị nghiệp vụ” mà bạn muốn “cùng lúc”.
2. “Distributed transaction” như tâm lý vs thực tế
Hai ý hay bị trộn:
- Phân tán nghiệp vụ (nhiều service) — luôn có eventual consistency ở đâu đó.
- Distributed atomic commit (2PC/XA) — đắt đỏ, kém chịu partition, ít phù hợp cho microservice latency-sensitive.
Middle+ cần từ bỏ ảo tưởng “một nút bấm ACID cho cả galaxy”.
3. Saga / compensation / outbox (bản đồ nhanh)
3.1. Saga (choreography / orchestration)
- Chuỗi bước: nếu bước sau fail → compensate các bước trước (hoặc đánh dấu trạng thái cần xử lý).
- Giá: logic phức tạp; idempotency mỗi bước; trạng thái trung gian hiển thị cho user.
3.2. Outbox
- Ghi business row + “event cần publish” cùng transaction trong DB.
- Dispatcher đọc outbox → publish message — giảm mất event khi crash giữa chừng.
3.3. Compensation không phải “undo máy thời gian”
Compensation là nghiệp vụ (hoàn tiền, hoàn điểm) — có thể tốn phí, có SLA, cần con người.
4. Cây quyết định (đơn giản hoá)
Có side-effect ngoài DB trong cùng use-case?
├─ KHÔNG → ưu tiên transaction DB + idempotency key cho HTTP
└─ CÓ
├─ Có thể gom vào một DB + outbox?
│ └─ CÓ → outbox + consumer idempotent
└─ KHÔNG (nhiều service độc lập)
├─ Chấp nhận trạng thái trung gian hiển thị cho user?
│ └─ CÓ → saga + compensation rõ ràng
└─ KHÔNG → hỏi lại product: có thật sự cần “atomic ảo” không?
5. Poison message và retry
Consumer fail vì bug logic:
- Retry vô hạn → làm hỏng downstream hoặc tốn chi phí.
- Cần DLQ (dead-letter), alert, replay có kiểm soát sau khi fix.
Ordering:
- “Cùng key vào cùng partition” giúp tuần tự nhưng tạo hotspot — trade-off chủ động.
Khi không nên saga
- Luồng đơn giản có thể đồng bộ một call + transaction DB + webhook idempotent.
- Khi team chưa có observability + runbook — saga chỉ nhân đôi incident không hiểu trạng thái.
6. Giải thích thêm: isolation level và “ảo tưởng serializable”
Trong một DB, bạn vẫn có thể chọn READ COMMITTED, REPEATABLE READ, SERIALIZABLE — mỗi mức đổi hành vi phantom read / write skew. Khi bạn nói “transaction đảm bảo mọi thứ”, hãy nói rõ mức isolation và giới hạn (ví dụ không serializable trên toàn cluster distributed SQL nếu không hiểu trade-off).
Đọc thêm nền: Quản lý transaction & concurrency.
7. Saga choreography vs orchestration
| Kiểu | Ý tưởng | Khi nào hợp |
|---|---|---|
| Choreography | Mỗi service tự publish/subscribe bước tiếp | Ít bước, coupling lỏng |
| Orchestration | Một “tổng đạo diễn” gọi tuần tự | Cần nhìn thấy toàn bộ flow một chỗ, dễ debug hơn cho team mới |
Cả hai đều cần idempotency ở mỗi bước và timeout rõ ràng; orchestrator không được trở thành SPOF không có retry/backoff.
8. Outbox: độ trễ và backpressure
Outbox làm atomic giữa “ghi business” và “ghi event”, nhưng introduce lag giữa commit DB và thời điểm message xuất hiện trên bus. Nếu consumer downstream yêu cầu “gần real-time”, bạn phải:
- Đo lag (timestamp outbox → publish).
- Scale dispatcher; tránh lock contention trên bảng outbox (partition theo shard, batch insert).
9. Inbox pattern (dedupe phía consumer)
Bổ sung cho outbox: bảng inbox lưu message_id đã xử lý — tương đương idempotency ở tầng message. Kết hợp outbox + inbox giúp at-least-once an toàn hơn khi consumer retry.
Tham chiếu chéo: Idempotency.
10. Ví dụ trạng thái trung gian user nhìn thấy
Khi saga tách “charge” và “activate license”, user có thể thấy “Thanh toán thành công — đang kích hoạt”. Đó là thiết kế UX của consistency chứ không phải bug — miễn SLA kích hoạt và runbook hỗ trợ rõ.
11. Ma trận: chọn pattern theo coupling
| Coupling giữa các side-effect | Gợi ý |
|---|---|
| Cùng DB schema | Transaction + outbox |
| Khác DB nhưng cùng team | Orchestrated saga + compensation |
| Khác org / SLA partner | Async + reconciliation + support playbook |
Bài tập ngắn
Vẽ use-case “thanh toán + gửi email + cập nhật CRM partner”: chỉ ra boundary ACID, chỗ cần outbox, và một compensation cụ thể nếu email fail sau khi charge.
Mở rộng: Liệt kê 3 trạng thái trung gian hợp lệ và message hiển thị cho user tương ứng.
12. Timeout cascade: saga không được quên hồi tiếp
Khi orchestrator gọi service B với timeout 2s nhưng B gọi C với timeout 5s, bạn sẽ có hành vi “rối loạn” — B timeout trong khi C vẫn chạy. Giải thích: thiết kế timeout theo chuỗi phụ thuộc, không đặt độc lập từng layer.
13. Two-phase commit (2PC) — chỉ nhắc ranh giới
2PC có thể phù hợp bên trong một technology cụ thể (một số distributed DB) nơi protocol được tối ưu sẵn. Ở lớp microservice tự chế, 2PC thường kém hấp dẫn vì blocking và coupling availability. Middle+ cần biết từ tồn tại để không đề xuất nó như default.
14. Read-your-writes và UX sau transaction
Sau khi user submit, họ F5 — họ kỳ vọng thấy dữ liệu mới. Nếu read model phía sau replica lag, họ thấy “mất dữ liệu”. Đây không phải saga bug, mà là consistency UX. Giải pháp: đọc từ primary, hoặc session stickiness read, hoặc hiển thị “đang đồng bộ…”.
15. Bảng: lỗi hay gặp khi triển khai outbox
| Lỗi | Triệu chứng | Hướng xử |
|---|---|---|
| Dispatcher single-thread | lag outbox tăng tuyến tính theo QPS | partition / batch |
| Transaction quá dài | lock contention | nhỏ hoá transaction |
| Duplicate publish | consumer thấy 2 event | idempotent consumer |
Tóm tắt cho người vội
- ACID kết thúc ở boundary DB; ra khỏi boundary là thế giới retry và eventual consistency.
- Outbox giải “mất event”; inbox/dedupe giải “trùng event”.
- Saga là chi phí nhận thức — chỉ khi product chấp nhận trạng thái trung gian hoặc không gom được transaction.
16. Một câu nhắc nhở khi họp thiết kế
“Nếu service B chết 30 phút, trạng thái hiển thị cho user và dữ liệu trong DB A là gì?” — nếu không trả lời được trong 2 phút, bạn chưa sẵn sàng chọn saga hay outbox; cần làm rõ failure story trước.
Đọc thêm
- Quản lý transaction & concurrency
- CDC và Outbox
- Enterprise Integration Patterns — Hohpe & Woolf (saga, messaging)
- Pat Helland — Life Beyond Distributed Transactions (tư duy giới hạn)