Skip to content

Distributed Transactions — Giao dịch phân tán

Khi bạn đặt một đơn hàng trên Shopee, hàng loạt hệ thống phải phối hợp trong vài giây: Order Service tạo đơn, Payment Service trừ tiền, Inventory Service giữ hàng, Notification Service gửi email xác nhận. Nếu Payment thành công nhưng Inventory phát hiện hết hàng — chuyện gì xảy ra? Trong monolith, một ROLLBACK giải quyết tất cả. Trong microservices, tiền đã trừ ở database A, hàng chưa giữ ở database B — không có cơ chế rollback nào hoạt động xuyên suốt nhiều databases.

Distributed transactions là bài toán khó nhất trong thiết kế hệ thống phân tán. Không phải vì thiếu giải pháp — mà vì mỗi giải pháp đều có trade-off nghiêm trọng. Two-Phase Commit đảm bảo consistency nhưng giết chết availability. Saga Pattern cho phép scalability nhưng phải đối mặt với eventual consistency và compensating logic phức tạp.

Bài này trang bị cho bạn kiến thức để chọn đúng pattern cho từng tình huống cụ thể — không phải một giải pháp "one size fits all".

Bức tranh tư duy

Hãy tưởng tượng bạn tổ chức một đám cưới với nhiều nhà thầu: nhà hàng, ban nhạc, thợ ảnh, trang trí hoa. Trong thế giới monolith, bạn ký một hợp đồng duy nhất với wedding planner — nếu có vấn đề, planner xử lý tất cả. Đó là ACID transaction.

Trong thế giới microservices, bạn ký hợp đồng riêng với từng nhà thầu. Nếu ban nhạc hủy vào phút chót, bạn phải tự gọi điện cho nhà hàng giảm số bàn, cancel thợ ảnh buổi after-party, và đổi layout hoa. Mỗi cuộc gọi đó là một compensating transaction — hành động hoàn tác hậu quả của bước trước.

Saga Pattern chính là quy trình bạn lập kế hoạch cho mọi tình huống: "Nếu bước 3 fail, gọi lại bước 2 và bước 1 để undo." Còn Two-Phase Commit là khi bạn yêu cầu TẤT CẢ nhà thầu cam kết trước (phase 1: "Các anh sẵn sàng chưa?"), rồi mới xác nhận (phase 2: "OK, tiến hành!"). Nhưng nếu thợ ảnh không trả lời điện thoại ở phase 2 — tất cả bị block.

Giới hạn phép so sánh: Đám cưới có thể dời lịch — production transactions không thể. Compensating transaction trong thực tế phức tạp hơn nhiều vì phải xử lý concurrent access, network failures, và partial state.

Cốt lõi kỹ thuật

Two-Phase Commit (2PC) — Consistency tuyệt đối, giá đắt

2PC đảm bảo atomicity xuyên suốt nhiều participants bằng hai giai đoạn:

Phase 1 — Prepare (Vote): Coordinator hỏi tất cả participants: "Bạn có thể commit không?" Mỗi participant kiểm tra điều kiện, lock resources, ghi WAL (Write-Ahead Log), và trả lời YES hoặc NO.

Phase 2 — Commit/Abort: Nếu TẤT CẢ trả lời YES → Coordinator gửi COMMIT. Nếu BẤT KỲ participant nào trả lời NO hoặc timeout → Coordinator gửi ABORT.

Vấn đề nghiêm trọng của 2PC:

  • Blocking protocol: Coordinator crash sau Phase 1 → tất cả participants giữ lock vô thời hạn.
  • Single point of failure: Coordinator down = hệ thống đông cứng.
  • Performance: Lock giữ suốt 2 phases — latency cao, throughput thấp.
  • Không phù hợp microservices: Yêu cầu tất cả participants available đồng thời.

Three-Phase Commit (3PC) — Cải tiến nhưng chưa đủ

3PC thêm phase Pre-Commit giữa Prepare và Commit, giảm blocking window:

  1. CanCommit: Coordinator hỏi participants có thể commit không (nhẹ hơn Prepare, không lock).
  2. PreCommit: Nếu tất cả đồng ý → ghi pre-commit record. Participants bắt đầu lock resources.
  3. DoCommit: Coordinator gửi final commit. Nếu Coordinator crash ở phase này, participants tự quyết định commit (đã có consensus ở phase 2).

Cải thiện so với 2PC: Giảm blocking khi Coordinator crash. Nhưng 3PC không giải quyết network partition — hai nhóm participants có thể đưa ra quyết định khác nhau. Trong thực tế, 3PC ít được sử dụng vì complexity cao mà fault tolerance cải thiện không đáng kể.

Saga Pattern — Giải pháp thực tế cho Microservices

Saga chia distributed transaction thành chuỗi local transactions. Mỗi local transaction cập nhật database của service đó và publish event/command để trigger bước tiếp theo. Nếu bước N fail, Saga thực thi compensating transactions cho bước N-1, N-2, ..., 1.

Choreography — Event-driven, không có central coordinator:

Orchestration — Central Saga Orchestrator điều phối:

Tiêu chíChoreographyOrchestration
CouplingLoose — services chỉ biết eventsTighter — orchestrator biết tất cả steps
VisibilityKhó debug (events phân tán)Dễ debug (saga state trong orchestrator)
ComplexityTăng theo số servicesTập trung ở orchestrator
Phù hợp3-5 bước, simple flows5+ bước, complex business logic

Compensating Transactions — Nghệ thuật hoàn tác

Compensating transaction không phải rollback. Nó là transaction mới tạo ra kết quả ngược lại về mặt nghĩa vụ kinh doanh (semantic undo), không phải undo ở database level.

Ví dụ cụ thể:

  • Payment charged $100 → Compensating: Refund $100 (INSERT refund record, không phải DELETE)
  • Inventory reserved 5 items → Compensating: Release 5 items (UPDATE stock + 5)
  • Email confirmation sent → Không thể undo hoàn toàn (gửi email thông báo hủy)

Quy tắc thiết kế:

  1. Idempotent: Gọi 2 lần phải cho cùng kết quả (network retry gửi duplicate)
  2. Commutative: Thứ tự compensate không ảnh hưởng kết quả cuối cùng
  3. Retryable: Compensating transaction cũng có thể fail — cần retry mechanism

Outbox Pattern — Đảm bảo Reliable Event Publishing

Vấn đề kinh điển: Service cập nhật database thành công nhưng fail khi publish event (hoặc ngược lại). Database và message broker mất đồng bộ.

Outbox Pattern giải quyết:

  1. Trong cùng database transaction: INSERT business data VÀ INSERT event vào outbox table.
  2. Background process (CDC hoặc polling) đọc outbox table và publish events.
  3. Sau khi publish thành công, đánh dấu event đã gửi.

Tại sao không publish trực tiếp vào Kafka: Database commit có thể thành công nhưng Kafka publish fail → data inconsistency. Outbox đảm bảo at-least-once delivery vì event được persist trong cùng database transaction với business data.

Thực chiến

Tình huống: E-commerce Order Flow — 50.000 orders/ngày

Bối cảnh: Sàn e-commerce, 50.000 orders/ngày (peak 500 orders/phút trong flash sale). Kiến trúc microservices: Order, Payment, Inventory, Shipping, Notification — mỗi service database riêng.

Mục tiêu: Zero data inconsistency, latency < 2s cho order confirmation, availability 99.95%.

Saga Orchestration Implementation:

Order Saga Steps:
1. Order Service   → Create Order (PENDING)        | Compensate: Cancel Order
2. Payment Service → Charge Customer ($)            | Compensate: Refund ($)
3. Inventory Service → Reserve Items                | Compensate: Release Items
4. Shipping Service → Create Shipment Label         | Compensate: Cancel Shipment
5. Notification Service → Send Confirmation Email   | Compensate: (no-op)

State Machine:
STARTED → ORDER_CREATED → PAYMENT_CHARGED → INVENTORY_RESERVED →
SHIPPING_CREATED → NOTIFICATION_SENT → COMPLETED

Failure at any step → compensate previous steps in reverse → FAILED

Số liệu production (ước lượng):

  • Saga completion time (happy path): 800ms-1.5s
  • Compensation trigger rate: ~0.3% orders (hết hàng, payment fail)
  • Compensation completion time: 2-5 giây
  • Orphaned sagas (cần manual intervention): < 0.01%

Tình huống: Banking Transfer — Consistency là bắt buộc

Bối cảnh: Chuyển tiền liên ngân hàng. Trừ tiền Account A (Bank A), cộng tiền Account B (Bank B). Không chấp nhận mất tiền hoặc sinh tiền.

Phương án: Saga + Idempotency Keys + Reconciliation

Step 1: Debit Account A  → Key: "TXN-{uuid}-DEBIT"
        Compensate: Credit back Account A

Step 2: Credit Account B → Key: "TXN-{uuid}-CREDIT"
        Compensate: Debit back Account B

Step 3: Reconciliation Job (mỗi 5 phút):
        - So sánh debit vs credit records
        - Flag transactions bất đối xứng
        - Alert team nếu mismatch > 0

Idempotency Key đảm bảo: retry sau network timeout nhận "already processed" thay vì duplicate transaction. Banking không dùng eventual consistency thuần túy — họ dùng saga + reconciliation + manual escalation, defense-in-depth đảm bảo consistency ở nhiều layer.

Sai lầm điển hình

Sai lầm 1: Dùng 2PC cho Microservices

Vấn đề: Áp dụng 2PC giữa các microservices qua network.

Tại sao sai: 2PC yêu cầu tất cả participants available đồng thời. Service deploy version mới trong khi 2PC đang chạy → connection lost → participants blocked. Latency 2PC qua network: 50-200ms per transaction — throughput collapse.

ĐÚNG: Saga Pattern cho cross-service transactions. Giữ 2PC chỉ trong cùng database hoặc cùng process.

Sai lầm 2: Compensating transaction không idempotent

Vấn đề: Refund function thực thi mỗi lần được gọi, không check trạng thái.

python
# SAI: Gọi 2 lần = refund 2 lần
def refund_payment(order_id, amount):
    account.balance += amount
    db.save(account)

Tại sao sai: Network retry, message redelivery đều gửi duplicate. Refund $100 hai lần = mất $100.

python
# ĐÚNG: Idempotent — check trạng thái trước
def refund_payment(order_id, amount, idempotency_key):
    existing = db.find_refund(idempotency_key)
    if existing:
        return existing
    refund = Refund(order_id=order_id, amount=amount, key=idempotency_key)
    account.balance += amount
    db.save_in_transaction(account, refund)
    return refund

Sai lầm 3: Publish event trước khi commit database

Vấn đề: Publish event vào Kafka rồi mới commit database.

python
# SAI: Event published nhưng DB commit có thể fail
def create_order(order_data):
    kafka.publish("OrderCreated", order_data)
    db.insert(order_data)  # Có thể fail!

Tại sao sai: Database commit fail → consumers nhận event cho order không tồn tại → data inconsistency toàn hệ thống.

python
# ĐÚNG: Outbox Pattern — event trong cùng transaction
def create_order(order_data):
    with db.transaction():
        db.insert("orders", order_data)
        db.insert("outbox", {
            "event_type": "OrderCreated",
            "payload": serialize(order_data),
            "status": "PENDING"
        })
    # CDC/poller publish từ outbox table

Sai lầm 4: Saga không có timeout

Vấn đề: Saga đợi response từ downstream service vô thời hạn.

Tại sao sai: Service down/slow → saga stuck PENDING → inventory locked → cascade ảnh hưởng orders khác. Mỗi Saga step phải có timeout rõ ràng (5-30 giây tùy SLA). Sau timeout → compensation hoặc retry.

ĐÚNG: Configure timeout per step, persist saga state, implement dead letter queue cho unprocessable messages.

Under the Hood

Performance Implications

PatternLatency/transactionThroughputConsistencyAvailability
Local ACID< 10ms10.000+ TPSStrongSingle DB uptime
2PC50-200ms100-1.000 TPSStrongLowest (all up)
3PC80-300ms50-500 TPSStrongBetter than 2PC
Saga (Choreography)200ms-2s5.000+ TPSEventualHigh
Saga (Orchestration)300ms-3s3.000+ TPSEventualHigh

Scaling Limits

2PC: Không scale horizontally. Coordinator là bottleneck. Lock duration tăng tuyến tính theo số participants.

Saga: Scale tốt horizontally. Mỗi step là independent local transaction. Bottleneck: message broker throughput (Kafka xử lý hàng triệu messages/giây) và saga state storage.

Outbox + CDC: Debezium CDC lag thông thường 10-100ms. Polling approach: phụ thuộc interval (100ms-5s). Scaling: partition outbox table theo aggregate_id.

Chi phí vận hành (ước lượng)

ComponentManaged ServiceSelf-hosted
Kafka cluster (3 brokers)$1.000-3.000/tháng1 SRE part-time
CDC (Debezium)$500-1.500/tháng computeIncluded with Kafka Connect
Saga State Store$100-500/tháng (Redis/PG)Minimal
DLQ Monitoring2-4 giờ/tuần engineer time2-4 giờ/tuần engineer time

Trade-offs Decision Matrix

Tiêu chíChọn 2PC khi...Chọn Saga khi...Chọn Outbox khi...
ConsistencyMust ACID (banking core)Eventual OKCần reliable publishing
Scale< 100 TPS1.000+ TPSBất kỳ scale
Participants2-3, cùng network3+, distributedN/A (bổ trợ Saga)
Latency200ms+ chấp nhận1-5s chấp nhậnThêm 10-100ms

Checklist ghi nhớ

✅ Checklist triển khai

Thiết kế Saga

  • [ ] Mỗi step có compensating transaction rõ ràng và đã test
  • [ ] Tất cả compensating transactions là idempotent
  • [ ] Timeout cụ thể cho mỗi step (document trong runbook)
  • [ ] Saga state persist (không giữ trong memory)
  • [ ] Dead Letter Queue cho messages không xử lý được

Outbox Pattern

  • [ ] Event và business data trong cùng database transaction
  • [ ] CDC hoặc polling hoạt động và được monitor
  • [ ] Outbox table có retention policy

Idempotency

  • [ ] Mỗi operation có idempotency key
  • [ ] Retry logic với exponential backoff
  • [ ] Duplicate detection ở consumer side

Observability

  • [ ] End-to-end tracing cho saga execution
  • [ ] Alert khi compensation rate vượt ngưỡng (> 1%)
  • [ ] Dashboard: saga completion time P50/P95/P99
  • [ ] Reconciliation job chạy định kỳ, alert khi mismatch

Bài tập luyện tập

Bài 1: Thiết kế Saga — Intermediate

Đề bài: Thiết kế Saga cho hệ thống đặt vé máy bay: (1) Tạo booking, (2) Giữ chỗ trên chuyến bay, (3) Charge thẻ tín dụng, (4) Issue e-ticket. Liệt kê compensating transaction cho mỗi step. Xử lý case: "Charge thẻ thành công nhưng Issue e-ticket fail."

🧠 Quiz

Câu hỏi kiểm tra: Nếu Step 3 (Payment) thành công nhưng Step 4 (Issue ticket) fail, hành động đúng là gì?

  • [ ] A. Retry Step 4 vô hạn cho đến khi thành công
  • [ ] B. Rollback database của tất cả services
  • [x] C. Compensating transactions: Refund → Release seat → Cancel booking
  • [ ] D. Alert admin và đợi xử lý thủ công Giải thích: Saga không có distributed rollback. Compensating transactions chạy ngược từ step cuối fail. Option A giữ resources locked vô hạn. Option B không khả thi cross-database. Option D là fallback cuối cùng, không phải primary strategy.
💡 Gợi ý
  • Xác định step nào reversible, step nào irreversible
  • Compensating cho "Issue e-ticket" cần xử lý case ticket đã gửi
  • Tất cả compensating transactions phải idempotent
✅ Lời giải chi tiết
Saga Steps & Compensations:

Step 1: Create Booking (PENDING)
  └── Compensate: Cancel Booking (CANCELLED)

Step 2: Reserve Seat (flight_id, seat_number)
  └── Compensate: Release Seat

Step 3: Charge Credit Card ($amount, idempotency_key)
  └── Compensate: Refund Credit Card

Step 4: Issue E-Ticket (booking_id)
  └── Compensate: Void E-Ticket + Cancellation email

Failure at Step 4:
1. Void E-Ticket (nếu đã issue) hoặc no-op
2. Refund Credit Card (idempotent)
3. Release Seat
4. Cancel Booking

Timeouts: Step 1: 5s | Step 2: 10s | Step 3: 30s | Step 4: 15s

Lưu ý: E-ticket đã gửi email → compensate bao gồm cancellation email. Đây là business logic không thể "undo" hoàn toàn ở technical level.

Bài 2: Outbox Pattern Implementation — Advanced

Đề bài: Thiết kế schema cho Outbox table và viết pseudocode cho polling-based publisher. Handle concurrent pollers, ensure at-least-once delivery, implement retry với exponential backoff.

💡 Gợi ý
  • Outbox table: id, event_type, payload, status, created_at, retry_count
  • Dùng SELECT ... FOR UPDATE SKIP LOCKED cho concurrent pollers
  • Max retry count để tránh infinite loop
✅ Lời giải chi tiết
sql
CREATE TABLE outbox (
    id            BIGSERIAL PRIMARY KEY,
    event_type    VARCHAR(100) NOT NULL,
    aggregate_id  VARCHAR(100) NOT NULL,
    payload       JSONB NOT NULL,
    status        VARCHAR(20) DEFAULT 'PENDING',
    retry_count   INT DEFAULT 0,
    max_retries   INT DEFAULT 5,
    created_at    TIMESTAMP DEFAULT NOW(),
    published_at  TIMESTAMP NULL,
    next_retry_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_outbox_pending
    ON outbox (status, next_retry_at) WHERE status = 'PENDING';
python
def poll_and_publish():
    while True:
        with db.transaction():
            events = db.query(
                "SELECT * FROM outbox "
                "WHERE status = 'PENDING' AND next_retry_at <= NOW() "
                "ORDER BY created_at LIMIT 50 "
                "FOR UPDATE SKIP LOCKED"
            )
            for event in events:
                try:
                    kafka.publish(event.event_type, event.payload)
                    mark_published(event.id)
                except PublishError:
                    handle_retry(event)
        time.sleep(0.1)  # 100ms poll interval

Phân tích: FOR UPDATE SKIP LOCKED cho phép multiple pollers song song không deadlock. Exponential backoff: 2s → 4s → 8s → 16s → 32s. Sau max retries → DEAD_LETTER để investigation.

Bài 3: Reconciliation System — Advanced

Đề bài: Thiết kế reconciliation job cho banking transfer system. Job chạy mỗi 5 phút, so sánh debit records (Bank A) và credit records (Bank B), flag mismatches, và escalate khi cần.

🧠 Quiz

Câu hỏi: Reconciliation job phát hiện debit record tồn tại nhưng không có credit record tương ứng. Nguyên nhân nào KHÔNG khả thi?

  • [ ] A. Credit service đang xử lý (eventual consistency lag)
  • [ ] B. Network timeout sau khi debit thành công
  • [x] C. Database của Bank A bị corrupt
  • [ ] D. Saga compensation đã chạy nhưng chưa cập nhật debit status Giải thích: Nếu Bank A database corrupt, debit record có thể không tồn tại hoặc sai — nhưng bài toán nói debit record tồn tại. Nguyên nhân phổ biến: lag, timeout, hoặc compensation race condition.
✅ Lời giải chi tiết
Reconciliation Logic:

1. Query debit records (last 10 minutes, status: COMPLETED)
2. Query credit records (last 10 minutes, status: COMPLETED)
3. Match by transaction_id (idempotency key)
4. Categories:
   - MATCHED: debit + credit exist → OK
   - DEBIT_ONLY: debit exists, no credit
     → If age < 5 min: PENDING (wait for eventual consistency)
     → If age > 5 min: ALERT (possible failure)
   - CREDIT_ONLY: credit exists, no debit
     → CRITICAL ALERT (money created from nothing?)
   - AMOUNT_MISMATCH: both exist but amounts differ
     → CRITICAL ALERT
5. Alert → PagerDuty → On-call engineer investigation

Liên kết học tiếp

Từ khóa glossary: distributed transaction, two-phase commit, 2PC, 3PC, saga pattern, choreography, orchestration, compensating transaction, outbox pattern, CDC, idempotency, eventual consistency, dead letter queue

Tìm kiếm liên quan: giao dịch phân tán, saga pattern là gì, outbox pattern implementation, 2PC vs saga, compensating transaction design