Backend

System Design — Bài 12: Saga, Outbox và resilience: giữ dữ liệu đúng khi service lỗi

SSite Admin
10 tháng 10, 2026 8 phút đọc 0 lượt xem

Đơn đã được lưu nhưng service chết trước khi phát event. Hoặc thanh toán thành công còn bước giao hàng thất bại. Những cửa sổ lỗi này cần trạng thái và quy trình phục hồi rõ ràng. Bài này nối giao dịch cục bộ, phối hợp nghiệp vụ và giới hạn lỗi lan truyền.

Khóa học System Design: mục lục 21 bài — bạn đang đọc bài 12.

Từ ACID tới 2PC

Sơ đồ: Từ ACID tới 2PC

Xem sơ đồ đầy đủ: Từ ACID tới 2PC

Two-Phase Commit có hai giai đoạn. Ở prepare, coordinator hỏi participant có thể cam kết hay không. Nếu đủ điều kiện, coordinator đưa ra quyết định commit; nếu không thì abort theo giao thức. Các bên cần lưu trạng thái phục hồi để xử lý sau lỗi.

Điểm khó là participant đã prepare có thể phải giữ tài nguyên trong lúc chưa biết quyết định cuối. Nếu coordinator mất liên lạc, giao dịch có thể ở trạng thái in-doubt và chờ recovery. Mức khóa và tác động throughput phụ thuộc implementation, nhưng thời gian phối hợp dài là chi phí quan trọng.

2PC giải quyết atomic commit, không thay thế concurrency control. Không nên xếp 2PC, Try-Confirm-Cancel và Saga vào một thang đơn giản từ mạnh tới yếu; chúng tổ chức cam kết và xử lý lỗi khác nhau, phù hợp các yêu cầu khác nhau.

Saga: giao dịch cục bộ và hành động bù

Sơ đồ: Saga: giao dịch cục bộ và hành động bù

Xem sơ đồ đầy đủ: Saga: giao dịch cục bộ và hành động bù

Saga chia quy trình thành các local transaction. Sau mỗi bước đã commit, hệ thống có thể nhìn thấy trạng thái trung gian. Nếu bước sau thất bại, ta thực hiện compensating action phù hợp với nghiệp vụ.

Ví dụ giữ hàng xong nhưng thanh toán thất bại thì hoàn giữ hàng. Hoàn tiền sau khi đã thu không phải xóa lịch sử thu tiền; nó là một tác động mới có thể cũng thất bại hoặc cần thời gian. Vì vậy compensation phải có trạng thái, retry và idempotency.

Pivot là ranh giới nghiệp vụ sau đó ta chủ yếu cố hoàn tất các bước còn lại thay vì quay lại trạng thái ban đầu. Chọn pivot phụ thuộc quy trình, không mặc định thu tiền luôn là điểm không thể bù. Saga tránh một global transaction kéo dài, nhưng vẫn có local lock, chờ đợi và xung đột cần quản lý.

Orchestration hay Choreography

Sơ đồ: Orchestration hay Choreography

Xem sơ đồ đầy đủ: Orchestration hay Choreography

Orchestration có một thành phần giữ trạng thái quy trình và quyết định bước tiếp theo. Ưu điểm là dễ thấy luồng tổng thể, timeout và recovery. Thành phần này cần lưu trạng thái bền, có thể phục hồi và không trở thành nơi gom mọi quy tắc của các domain.

Choreography để service phản ứng với event rồi phát event tiếp. Nó phù hợp khi các phản ứng khá độc lập, nhưng khi chuỗi phụ thuộc dài sẽ khó biết toàn bộ quy trình đang đứng ở đâu và ai chịu trách nhiệm khôi phục.

Cả hai đều cần correlation ID, retry, dedup và khả năng quan sát. Message còn trong broker không đồng nghĩa trạng thái nghiệp vụ đã được quản lý đầy đủ. Chọn theo độ phức tạp của luồng và năng lực vận hành; không có phương án luôn ưu việt hơn.

Saga mất Isolation — và hai hiểu lầm

Sơ đồ: Saga mất Isolation — và hai hiểu lầm

Xem sơ đồ đầy đủ: Saga mất Isolation — và hai hiểu lầm

Khi các bước commit riêng, quy trình khác có thể thấy trạng thái chưa hoàn tất toàn bộ. Nếu đang phê duyệt đơn mà đồng thời nhận yêu cầu hủy, cần state machine và điều kiện chuyển trạng thái. Semantic lock như trạng thái pending giúp diễn đạt quyền thao tác, nhưng vẫn phải cập nhật nguyên tử để tránh hai bên cùng đi tiếp.

Outbox giải lỗi dual-write bằng cách lưu thay đổi nghiệp vụ và event cần phát trong cùng transaction database. Relay gửi event sau; nếu gửi xong rồi chết trước khi đánh dấu, event có thể được gửi lại. Consumer cần dedup cùng transaction với tác động nghiệp vụ.

Event sourcing là một cách lưu trạng thái qua sự kiện; nó không tự điều phối transaction xuyên service. Tương tự, thêm distributed transaction không tự làm throughput tăng nếu mọi giao dịch vẫn tranh một bản ghi nóng.

Resilience — giữ hệ thống sống

Resilience bắt đầu bằng giới hạn thời gian và tài nguyên. Mỗi lời gọi cần timeout phù hợp ngân sách toàn request. Retry chỉ nên áp dụng khi lỗi có khả năng tạm thời và thao tác có cách chống tác động trùng.

Nếu mỗi tầng thử lại ba lần trong một chuỗi nhiều tầng, số lần gọi xuống cuối có thể nhân lên rất nhanh. Vì vậy cần retry budget, exponential backoff và jitter để tránh tất cả client quay lại cùng lúc. Queue và connection pool cũng cần giới hạn thay vì để chờ vô hạn.

Circuit breaker hạn chế tiếp tục gọi một phụ thuộc đang lỗi. Load shedding chủ động bỏ bớt công việc khi chính mình quá tải. Có thể hạ cấp gợi ý nhưng vẫn xử lý đặt hàng; riêng dữ liệu cốt lõi không được thay bằng kết quả giả chỉ để response nhìn như thành công.

Circuit Breaker & Load Shedding — cầu dao và van xả

Sơ đồ: Circuit Breaker & Load Shedding — cầu dao và van xả

Xem sơ đồ đầy đủ: Circuit Breaker & Load Shedding — cầu dao và van xả

Ở trạng thái closed, circuit breaker cho lời gọi qua và theo dõi kết quả. Khi đạt điều kiện lỗi, nó chuyển open để fail nhanh trong một khoảng. Half-open cho một số lượt thử có kiểm soát, rồi quyết định mở lại hay tiếp tục chặn.

Ngưỡng lỗi 50% trong 10 giây chỉ là ví dụ cấu hình, không phải mặc định phù hợp mọi hệ thống. Cần đủ mẫu và phân loại lỗi đúng; lỗi nhập liệu của client không nhất thiết cho thấy dependency hỏng. Circuit breaker cũng không thay timeout hoặc giới hạn số lời gọi đồng thời.

Load shedding giải quá tải tại chính service: từ chối sớm, giới hạn hàng đợi hoặc ưu tiên chức năng quan trọng. Phản hồi lỗi nhanh có thể tốt hơn giữ request tới hết timeout, miễn client nhận hướng xử lý rõ. Fallback phải trung thực với mức chức năng còn phục vụ được.

Event Sourcing — lưu sự kiện thay vì lưu trạng thái

Sơ đồ: Event Sourcing — lưu sự kiện thay vì lưu trạng thái

Xem sơ đồ đầy đủ: Event Sourcing — lưu sự kiện thay vì lưu trạng thái

Thay vì chỉ lưu số dư cuối là 70, event sourcing lưu các sự kiện như nạp 100, rút 50, nạp 20. Trạng thái được dựng bằng cách áp dụng chúng theo quy tắc nghiệp vụ.

Điều này giúp lần lại nguyên nhân và dựng projection cho nhu cầu đọc khác nhau. Chuỗi dài thì snapshot giảm phần phải replay. Đổi lại, schema sự kiện, phiên bản xử lý và sự kiện sai phải được quản lý lâu dài. Replay không được vô tình gửi lại email hoặc gọi thanh toán bên ngoài.

CQRS, Command Query Responsibility Segregation, tách mô hình ghi và đọc, thường đi cùng nhưng không bắt buộc phải dùng event sourcing. Ledger append-only có tinh thần tương tự, nhưng một ledger không mặc nhiên là toàn bộ kiến trúc event-sourced. Audit trail cũng cần kiểm soát quyền và tính toàn vẹn để có giá trị.

Hai transaction và một relay có thể gửi trùng

Order DB transaction:
  INSERT order
  INSERT outbox(event_id, payload)
  COMMIT

Relay: đọc outbox -> phát event -> đánh dấu đã gửi
                  [crash sau phát: event có thể gửi lại]

Consumer DB transaction:
  INSERT inbox(event_id UNIQUE)
  cập nhật nghiệp vụ
  COMMIT

Side effect ngoài DB: cần idempotency với bên nhận riêng.

Nếu insert inbox báo event đã xử lý thì bỏ qua tác động đã hoàn tất; nếu transaction trước rollback thì retry được thực hiện lại. Retention của dedup phải bao phủ cửa sổ retry và replay. Đây là kiểm soát tác động trùng, không phải lời hứa exactly-once cho mọi hệ bên ngoài.

Thuật ngữ cần nhớ

  • 2PC — Two-Phase Commit: Giao thức cam kết hai pha: chuẩn bị rồi quyết định commit/abort chung giữa các bên tham gia; có thể bị chặn khi lỗi điều phối, không tự định nghĩa mức isolation.

  • ACID — Atomicity, Consistency, Isolation, Durability: Bốn thuộc tính giao dịch: nguyên tử (tất cả hoặc không); nhất quán (giữ bất biến dữ liệu); cô lập (kiểm soát ảnh hưởng đồng thời); bền vững (dữ liệu đã commit tồn tại theo cam kết). Chữ C ở đây khác consistency trong định lý CAP.

  • CQRS — Command Query Responsibility Segregation: Tách trách nhiệm ghi và đọc; có thể dùng mô hình đọc tối ưu riêng, đồng thời phải xử lý độ trễ đồng bộ và cách phục hồi projection.

  • DLQ — Dead-Letter Queue: Hàng đợi chứa thông điệp không xử lý được sau chính sách retry; phục vụ điều tra, sửa và phát lại có kiểm soát.

Bài tập tự thực hành

Order đã commit nhưng relay chết sau khi gửi event.

Gợi ý kiểm tra lời giải

Event có thể gửi lại; consumer cần idempotency và ghi dedup cùng transaction nghiệp vụ.

Tự kiểm tra sau bài học

  • Compensation có trạng thái, retry và idempotency.

  • Outbox và nghiệp vụ commit cùng nhau; inbox và tác động cũng vậy.

  • Timeout, retry budget và giới hạn in-flight ngăn lỗi dây chuyền.

Đọc thêm từ tài liệu gốc

AWS: Transactional Outbox

Tiếp tục lộ trình

Bài trước — Nhất quán trong hệ phân tán: CAP, Raft và đồng hồ logic

Bài tiếp — Microservices hay Modular Monolith? Chọn ranh giới và tính chi phí

Xem toàn bộ khóa học System Design 21 bài

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 76: Circuit Breaker với Resilience4j

CLOSED, OPEN, HALF_OPEN, fallback, bulkhead và rate limiter để giới hạn lỗi dây chuyền.

10 thg 10, 20262 phút5
99 Ngày Java — Ngày 76: Soft, Weak và Phantom Reference

Bốn mức tham chiếu, WeakHashMap, ReferenceQueue và cách chọn cache có giới hạn trên Java 21.

10 thg 10, 20262 phút6
K

Học System Design qua 21 bài: yêu cầu, capacity, database, cache, hệ phân tán và 6 bài thực hành, kèm bài tập, sơ đồ luồng và mục lục đầy đủ.

10 thg 10, 20266 phút3