Mẫu Transactional Outbox (Transactional Outbox Pattern)
1. Tổng quan
Mẫu Transactional Outbox là một mẫu thiết kế hệ thống phân tán, trong đó việc thay đổi dữ liệu nghiệp vụ và việc phát hành sự kiện/thông điệp được ghi trong cùng một giao dịch cục bộ của cơ sở dữ liệu, sau đó một message relay riêng biệt chuyển các bản ghi outbox đã commit tới broker.
Trong microservice, một lệnh thường tạo ra hai tác dụng phụ. Thứ nhất, lưu trạng thái nghiệp vụ như đơn hàng, thanh toán, tồn kho vào cơ sở dữ liệu của chính dịch vụ. Thứ hai, phát hành sự kiện lên message broker để các dịch vụ khác thực hiện nghiệp vụ tiếp theo. Hai đích lưu trữ này là hai hệ thống khác nhau, nên nếu mã ứng dụng gọi lần lượt từng cái thì sẽ trở thành ghi kép (dual write).
Nếu tiến trình dừng ngay sau khi commit cơ sở dữ liệu thành công, dữ liệu tồn tại nhưng sự kiện có thể biến mất. Ngược lại, nếu phát hành lên broker thành công trước rồi giao dịch cơ sở dữ liệu bị rollback, consumer sẽ xử lý một sự kiện nghiệp vụ không hề tồn tại. Timeout mạng không cho bên gọi biết rõ broker đã thực sự nhận hay chưa, nên chỉ thử lại đơn thuần thì khó giải quyết đồng thời cả trùng lặp lẫn mất mát.
Transactional Outbox không gửi thông điệp trực tiếp tới broker mà lưu nó cùng lúc vào bảng hoặc bản ghi outbox trong cơ sở dữ liệu. Khi bảng nghiệp vụ và outbox nằm trong cùng một giao dịch cục bộ, mọi thay đổi nghiệp vụ đã commit chắc chắn để lại sự kiện cần chuyển đi, còn thay đổi bị rollback thì không để lại sự kiện nào. Sau đó message relay đọc outbox và gửi tới broker, nhờ vậy tách được luồng xử lý yêu cầu của ứng dụng khỏi sự cố của broker bên ngoài.
Mẫu này không phải là kỹ thuật vạn năng giải quyết mọi giao dịch phân tán. Nó nguyên tử hóa việc ghi dữ liệu và sự kiện bên trong một dịch vụ, nhưng không gắn nguyên tử việc chuyển tới broker với việc xử lý tại consumer. Vì vậy cần thiết kế đồng thời phân phối ít nhất một lần (at-least-once), thông điệp trùng lặp, thứ tự, tính lũy đẳng (idempotency) của consumer, xử lý lại và nghiệp vụ bù trừ.
Trong bài thi Kỹ sư chuyên nghiệp (Professional Engineer), điều quan trọng không phải chỉ viết định nghĩa mà là giải thích liên kết bốn trục: điểm thất bại của ghi kép, ghi nguyên tử vào outbox, gửi lại qua relay và tính lũy đẳng của consumer.
2. Bối cảnh ra đời và vấn đề cần giải quyết
2.1 Ghi kép trong phương thức phát hành trực tiếp
Cách hiện thực trực quan nhất là dịch vụ gọi API publish của message broker ngay sau khi cập nhật cơ sở dữ liệu.
Nếu cả hai lời gọi đều thành công thì đạt kết quả mong muốn, nhưng chỉ cần một trong các thành phần tiến trình ứng dụng, mạng, cơ sở dữ liệu hoặc broker gặp lỗi thì trạng thái sẽ bị phân kỳ.
Cách ghi cơ sở dữ liệu trước có một khoảng thời gian mà sau khi trạng thái nghiệp vụ đã commit, việc phát hành lên broker thất bại hoặc tiến trình bị chết. Khi đó dịch vụ tồn kho hay dịch vụ giao hàng có thể vĩnh viễn không biết đơn hàng đã được tạo. Cách ghi broker trước còn tạo ra sự kiện sai nghiêm trọng hơn, vì dữ liệu gốc có thể bị rollback trong khi consumer đang xử lý sự kiện.
sequenceDiagram
participant C as Client
participant S as Order Service
participant DB as Business DB
participant B as Message Broker
participant D as Downstream Service
C->>S: Lệnh tạo đơn hàng
S->>DB: Lưu đơn hàng
DB-->>S: commit thành công
S->>B: Phát hành OrderCreated
B-->>S: timeout hoặc sự cố
Note over DB,B: Dữ liệu đã commit nhưng không rõ sự kiện đã được chuyển hay chưa
B-->>D: Sự kiện không đến hoặc bị gửi lại
Điểm cốt lõi là DB commit và broker publish là các thao tác độc lập trên những tài nguyên khác nhau.
Dù đặt lời gọi broker bên trong giao dịch cơ sở dữ liệu, nếu broker không tham gia cùng giao dịch thì tính nguyên tử vẫn không hình thành.
Ngược lại, độ trễ của broker còn khiến kết nối cơ sở dữ liệu bị chiếm giữ lâu và làm thứ tự giữa commit và gửi thông điệp trở nên khó diễn giải.
Trái lại, phương thức outbox tách lời gọi broker ra khỏi giao dịch nghiệp vụ. Trước tiên hoàn tất một giao dịch cục bộ ngắn commit cùng lúc bảng nghiệp vụ và hàng outbox, sau đó relay chỉ gửi bất đồng bộ những hàng đã commit. Do đó điều kiện thành công của phản hồi yêu cầu không phải là "broker đã nhận ngay lập tức" mà được định nghĩa là "trạng thái nghiệp vụ và sự kiện cần chuyển đã được lưu an toàn".
2.2 Ma trận thất bại và phạm vi bảo đảm
Khi phân tích thất bại, cần quan sát tách biệt DB nghiệp vụ, relay, broker và consumer. Nếu giao dịch DB nghiệp vụ bị rollback thì outbox cũng phải rollback theo và sự kiện không được phát hành. Nếu relay chết sau khi commit thì hàng outbox vẫn còn và phải có thể thử lại sau khi khởi động lại.
| Giai đoạn | Ví dụ sự cố | Kết quả mong muốn | Thiết kế cần thiết |
|---|---|---|---|
| Trước khi lưu nghiệp vụ | Lỗi kiểm tra đầu vào, vi phạm quy tắc miền | Không có cả dữ liệu nghiệp vụ lẫn sự kiện | Rollback giao dịch |
| Đang lưu nghiệp vụ | Lỗi DB, timeout | Không có cả dữ liệu nghiệp vụ lẫn outbox | Cùng một giao dịch cục bộ |
| Ngay sau commit | Tiến trình dịch vụ dừng | Outbox còn lại, việc gửi bị trì hoãn | Khởi động lại, thử lại |
| Đang gửi tới broker | Mất phản hồi mạng | Không rõ broker đã thực sự nhận hay chưa | Chấp nhận trùng lặp, ID thông điệp |
| Đang xử lý tiêu thụ | Consumer dừng | Thông điệp được phân phối lại hoặc xử lý lại | Tính lũy đẳng của consumer |
| Sự cố kéo dài | Broker/consumer ngừng hoạt động lâu | Outbox tồn đọng và phát cảnh báo | Lưu giữ, cách ly, runbook vận hành |
Bảo đảm tiêu biểu của mẫu này là "chỉ khi giao dịch nghiệp vụ được commit thì sự kiện mới trở thành đối tượng của relay". Tuy nhiên, bảo đảm "sự kiện đến broker và consumer đúng một lần" không đúng với các hiện thực thông thường. Lý do là nếu relay chết trước khi nhận phản hồi gửi thành công, sau khi khởi động lại nó có thể gửi lại cùng hàng đó.
Vì vậy, an toàn hơn là nêu rõ ngữ nghĩa phân phối của hệ thống là at-least-once và coi trùng lặp là tình huống vận hành bình thường. Kết quả nghiệp vụ trông như exactly-once được hiện thực tại consumer bằng cách kết hợp ID thông điệp, lịch sử xử lý, cập nhật có điều kiện và ràng buộc duy nhất. Nếu không phân biệt ngữ nghĩa phân phối với ngữ nghĩa nghiệp vụ, việc chỉ tin vào chức năng của broker sẽ dẫn đến lỗi cho phép thanh toán trùng hay đặt chỗ trùng.
3. Thành phần cốt lõi và luồng xử lý
3.1 Cấu trúc logic của outbox
Outbox là ranh giới bền vững lưu tạm thời các sự kiện cần gửi. Trong cơ sở dữ liệu quan hệ, nó được tạo thành một bảng riêng trong cùng cơ sở dữ liệu với bảng nghiệp vụ; trong cơ sở dữ liệu tài liệu, có thể biểu diễn bằng mảng sự kiện hoặc thuộc tính thay đổi bên trong bản ghi nghiệp vụ. Dù theo cách nào, cũng cần các định danh để theo dõi ý nghĩa nghiệp vụ và trạng thái gửi của sự kiện.
flowchart LR
C[Bộ xử lý lệnh] --> TX[Giao dịch DB cục bộ]
TX --> A[Lưu Aggregate nghiệp vụ]
TX --> O[Lưu Outbox\nmessage_id aggregate_id type payload]
O --> R[Message Relay]
R --> P{Gửi tới broker}
P -->|Thành công| S[Ghi sent_at hoặc xóa]
P -->|Thất bại| Q[Thử lại, backoff]
Q --> R
P --> M[Consumer xử lý lũy đẳng]
message_id là cơ sở cho việc gửi lại và loại bỏ trùng lặp tại consumer.
aggregate_id gom các sự kiện thuộc cùng một đơn hàng, tài khoản, lô giao hàng và có thể dùng làm khóa phân vùng hoặc cơ sở bảo đảm thứ tự.
event_type và schema_version giúp consumer diễn giải loại sự kiện và phiên bản hợp đồng.
occurred_at là thời điểm sự kiện nghiệp vụ xảy ra, còn published_at là thời điểm chuyển tới broker, nên không được nhầm lẫn hai giá trị.
sequence có thể biểu diễn thứ tự nghiệp vụ trong cùng một aggregate, nhưng một giá trị tự tăng toàn cục đơn thuần không có nghĩa là thứ tự nhân quả của mọi dịch vụ.
attempt_count, last_error, next_attempt_at được dùng để người vận hành kiểm tra trạng thái thử lại và tính toán exponential backoff.
Lược đồ ví dụ có thể thiết kế như sau.
| Cột | Ý nghĩa | Điểm thiết kế |
|---|---|---|
id |
Định danh hàng outbox | Khóa tăng dần hoặc ID có thể sắp xếp theo thời gian |
message_id |
ID thông điệp logic | Dùng cho ràng buộc duy nhất toàn cục và loại bỏ trùng lặp |
aggregate_type |
Loại đối tượng nghiệp vụ | Định tuyến consumer và rà soát quyền |
aggregate_id |
Định danh đối tượng nghiệp vụ | Thứ tự trong cùng đối tượng, khóa phân vùng |
event_type |
Loại sự kiện | Chọn hợp đồng và handler |
schema_version |
Phiên bản payload | Cơ sở tương thích ngược và chuyển đổi |
payload |
Nội dung sự kiện | Tối thiểu hóa thông tin nhạy cảm, quy tắc tuần tự hóa |
occurred_at |
Thời điểm phát sinh nghiệp vụ | Đo độ trễ và truy vết kiểm toán |
status |
pending·processing·sent·dead | Kiểm soát chuyển trạng thái và xử lý lại |
attempt_count |
Số lần thử gửi | Ngưỡng backoff và cách ly |
last_error |
Tóm tắt lỗi gần nhất | Loại trừ thông tin bí mật, chẩn đoán vận hành |
Payload của outbox nên chứa các sự thật mà consumer cần, và cần thận trọng khi chọn dạng payload chỉ có ý nghĩa đầy đủ khi phải truy vấn lại cơ sở dữ liệu gốc về sau. Lý do là nếu bản ghi gốc bị thay đổi hoặc xóa giữa thời điểm phát hành và thời điểm tiêu thụ, sẽ không thể tái hiện sự thật trong quá khứ. Ngược lại, sao chép dữ liệu cá nhân và nhị phân dung lượng lớn vào sự kiện làm phức tạp yêu cầu lưu giữ, mã hóa, xóa, nên cần áp dụng nguyên tắc tối thiểu hóa dữ liệu.
3.2 Thứ tự ghi nguyên tử
Dịch vụ kiểm tra lệnh, thực thi quy tắc miền, rồi đưa thay đổi nghiệp vụ và sự kiện outbox vào một giao dịch cục bộ. Dù lưu bảng nghiệp vụ trước và outbox sau, miễn cả hai cùng một giao dịch thì tính nguyên tử quan trọng hơn bản thân thứ tự. Bất kỳ thao tác ghi nào thất bại đều phải rollback toàn bộ, và phản hồi thành công chỉ được trả về sau khi commit này.
Tầng ứng dụng gọi phát hành sự kiện nhưng không trực tiếp gửi tới broker; cổng lưu sự kiện (port) và message relay được tách riêng. Ngay cả khi thu thập domain event, cần chốt correlation ID, causation ID, schema version tại điểm chuyển sự kiện thành bản ghi outbox. Có như vậy mới quan sát được việc truy vết cùng một lệnh và quan hệ nhân quả nối tiếp qua nhiều dịch vụ.
Nếu chỉ dựa vào auto flush của ORM hay ranh giới giao dịch, có thể xảy ra mất sự kiện trong môi trường kiểm thử và vận hành. Cần quyết định tường minh sự kiện sẽ phát hành cho mỗi thay đổi trạng thái nghiệp vụ, và trong kiểm thử tích hợp phải xác minh "số trạng thái đã commit = số sự kiện outbox" và khi rollback thì "outbox 0 bản ghi". Cũng nên dùng quy tắc review mã hoặc domain event dispatcher để giảm khả năng lập trình viên bỏ sót việc ghi outbox.
3.3 Phương thức message relay
Relay là một tiến trình riêng hoặc thành phần nền tảng dữ liệu chuyển outbox tới broker.
Polling publisher đơn giản nhất định kỳ truy vấn các hàng pending, áp dụng khóa/claim rồi phát hành theo lô.
Sau khi xác nhận phát hành thành công thì ghi sent_at hoặc xóa, còn các hàng thất bại được cập nhật thời điểm thử lại.
Chu kỳ polling ngắn giảm độ trễ phân phối nhưng tăng tải truy vấn DB và khóa. Kích thước lô quá lớn làm quy mô gửi lại và tải broker tăng mạnh sau một lần sự cố, còn quá nhỏ thì thông lượng thấp. Do đó cần xác định chu kỳ và kích thước lô bằng kiểm thử tải dựa trên độ trễ mục tiêu, tốc độ đổ vào outbox, kích thước payload trung bình, IOPS của DB và quota của broker.
Phương thức CDC (Change Data Capture) đọc các thay đổi outbox đã commit từ transaction log hoặc change stream của cơ sở dữ liệu rồi chuyển tới broker. So với polling, nó có thể giảm tải truy vấn DB của ứng dụng và độ trễ phân phối, nhưng phải quản lý riêng việc lưu giữ log, thay đổi lược đồ, vận hành connector và khôi phục offset. CDC không có nghĩa là biến ngay mọi thay đổi của bảng nghiệp vụ thành sự kiện công khai; an toàn hơn là chỉ giới hạn đối tượng phát hành ở các sự kiện outbox tường minh.
| Phương thức | Ưu điểm | Chi phí, rủi ro | Tình huống phù hợp |
|---|---|---|---|
| Polling định kỳ | Cấu trúc đơn giản, có thể bắt đầu chỉ với DB | Tải truy vấn/khóa, độ trễ dao động | Quy mô nhỏ hoặc giai đoạn đầu áp dụng |
| CDC dựa trên transaction log | Độ trễ thấp, có lợi cho xử lý khối lượng lớn | Độ phức tạp vận hành connector, log, offset | Tổ chức có lượng sự kiện lớn và năng lực nền tảng |
| Trigger DB | Có thể ngăn bỏ sót ghi | Quy tắc nghiệp vụ ẩn trong DB, giảm tính khả chuyển | Tích hợp hệ thống cũ (legacy) hạn chế |
| Thu thập sự kiện ở ứng dụng | Kiểm soát ý nghĩa nghiệp vụ và hợp đồng bằng mã | Lập trình viên bỏ sót, phụ thuộc thư viện chung | Dịch vụ hướng miền |
Giữa việc gửi của relay và việc ghi trạng thái lại có một khoảng không nguyên tử.
Relay có thể chết sau khi gửi thành công tới broker nhưng trước khi ghi sent_at, nên chỉ đổi hàng trạng thái sang processing trước không làm biến mất trùng lặp.
Thay vì cố loại bỏ khoảng này, hãy thiết kế cho phép gửi lại và giữ nguyên message_id để consumer loại bỏ trùng lặp an toàn.
3.4 Đồng thời, khóa, thứ tự
Nếu nhiều instance relay đọc cùng một hàng, việc phát hành trùng có thể gia tăng.
Trong DB quan hệ có thể kết hợp SELECT ... FOR UPDATE SKIP LOCKED với thời điểm claim, chủ sở hữu và hạn lease, nhưng phải kiểm chứng ngữ nghĩa khóa của DB đang dùng và chi phí của giao dịch kéo dài.
Nếu relay đang chiếm giữ hàng bị chết, relay khác phải có thể thu hồi hàng đó sau khi lease hết hạn.
Nếu thứ tự sự kiện trong cùng aggregate là quan trọng, chọn một trong các cách: một phân vùng duy nhất cho mỗi aggregate, kiểm tra sequence, hoặc claim tuần tự. Ép thứ tự toàn cục có thể làm giảm thông lượng và tính sẵn sàng, nên phần lớn chỉ bảo đảm thứ tự trong phạm vi cần thiết về nghiệp vụ như cùng một đơn hàng, tài khoản hay thiết bị. Không được giả định rằng thứ tự ID hàng giữa các aggregate khác nhau chính là quan hệ nhân quả nghiệp vụ.
4. Thiết kế trùng lặp, thứ tự và xử lý lại
4.1 Consumer lũy đẳng
Trùng lặp phát sinh một cách tự nhiên khi relay không xác nhận được phản hồi của broker, khi broker phân phối lại, hoặc khi consumer gặp sự cố trước khi ACK.
Consumer đặt ràng buộc duy nhất trên bảng lịch sử xử lý message_id để nhanh chóng coi các thông điệp đã xử lý là thành công, hoặc làm cho bản thân việc cập nhật nghiệp vụ trở nên có điều kiện và lũy đẳng.
Ví dụ, yêu cầu phê duyệt thanh toán đặt một khóa lần thử thanh toán duy nhất theo ID đơn hàng, và khi yêu cầu thứ hai đến với cùng khóa thì trả về kết quả đã có.
Việc ghi lịch sử xử lý và cập nhật nghiệp vụ cũng nên gộp vào một giao dịch cục bộ của consumer nếu có thể. Nếu sự cố xảy ra sau khi cập nhật nghiệp vụ nhưng trước khi ghi lịch sử xử lý, việc xử lý lại sẽ gây tác dụng phụ trùng lặp, vì vậy hãy tận dụng xung đột khóa duy nhất như một tín hiệu trùng lặp bình thường. Với tác dụng phụ nằm ngoài DB của consumer như API thanh toán bên ngoài, phải dùng kèm idempotency key của nhà cung cấp, khóa yêu cầu và tác vụ đối soát.
4.2 Phân loại thất bại và thử lại
Lỗi mạng tạm thời, broker throttling, consumer tạm dừng được thử lại với exponential backoff và jitter. Lỗi lược đồ, thiếu trường bắt buộc, trạng thái nghiệp vụ không hợp lệ thì thử lại cũng không thành công, nên thay vì thử lại vô hạn hãy chuyển vào hàng đợi cách ly hoặc dead-letter outbox. Chính sách thử lại phải bao gồm số lần tối đa theo mã lỗi, thời gian lưu giữ tối đa và việc có cần người vận hành phê duyệt xử lý lại hay không.
stateDiagram-v2
[*] --> PENDING
PENDING --> PROCESSING: claim lease
PROCESSING --> SENT: broker ack
PROCESSING --> PENDING: transient failure
PROCESSING --> DEAD: permanent failure or max attempts
SENT --> [*]: archive or delete
DEAD --> PENDING: approved replay
DEAD --> [*]: retain for audit
Xử lý lại cần phân biệt giữa gửi lại nguyên payload gốc và tạo sự kiện hiệu chỉnh mới. Gửi lại đơn thuần hữu ích khi áp dụng lại các sự kiện cũ sau khi mã consumer đã được sửa, nhưng không hoàn tác được tác dụng phụ bên ngoài đã thực hiện một phần. Với trường hợp cần nghiệp vụ bù trừ như số tiền, tồn kho, quyền hạn, người vận hành phải xác nhận nguyên nhân và kết quả rồi thực thi một lệnh bù trừ riêng.
4.3 Khả năng quan sát và chỉ số vận hành
Outbox là ranh giới bất đồng bộ nên nếu chỉ nhìn tỷ lệ yêu cầu thành công sẽ bỏ lỡ sự cố.
Thu thập các chỉ số cốt lõi: oldest age, pending count, publish latency, retry rate, dead-letter count, relay throughput của các hàng outbox.
Tách độ trễ từ occurred_at đến thời điểm phát hành lên broker với độ trễ từ phát hành đến hoàn tất xử lý tiêu thụ sẽ giúp xác định vị trí nút thắt cổ chai.
Trong log, ghi có cấu trúc message_id, aggregate_id, correlation_id, event_type, attempt_count, relay_instance. Không để lại toàn bộ payload hay dữ liệu cá nhân trong log; dùng ID truy vết để liên kết sự kiện gốc với bản ghi vận hành. Trong distributed tracing, span yêu cầu nghiệp vụ và span relay nằm trên các tiến trình khác nhau, nên cần truyền trace context một cách an toàn qua header của sự kiện.
Xóa ngay outbox đã hết thời hạn lưu giữ có thể gây khó khăn cho kiểm toán và xử lý lại. Phân biệt vùng hot của DB vận hành, archive chi phí thấp và chính sách xóa dữ liệu cá nhân, đồng thời xem xét sao lưu, đối soát và khả năng xử lý lại trước khi xóa. Giữ vô hạn các hàng đã xử lý xong sẽ làm tăng chi phí chỉ mục và lưu trữ, vì vậy hãy dùng phân vùng, TTL, archive job nhưng không để xung đột với yêu cầu lưu giữ theo quy định.
5. Công nghệ liên quan và so sánh
5.1 So sánh với phát hành trực tiếp và 2PC
Phát hành trực tiếp có mã hiện thực ngắn nhưng ứng dụng phải tự gánh khoảng thất bại giữa commit DB và gửi broker. 2PC điều phối prepare và commit của nhiều bên tham gia để cố gắng commit nguyên tử, nhưng phải xem xét việc các bên có hỗ trợ hay không, sự cố coordinator, chi phí giữ khóa và chặn (blocking). Outbox không biến broker thành bên tham gia giao dịch phân tán mà ghi thông điệp vào DB trước, nhờ đó giảm mức độ liên kết và độ phức tạp vận hành.
| Tiêu chí | Phát hành trực tiếp | 2PC/XA | Transactional Outbox |
|---|---|---|---|
| Phạm vi nguyên tử | Phụ thuộc thứ tự gọi | Toàn bộ các bên tham gia | Ghi DB nghiệp vụ và outbox |
| Liên kết với broker | Liên kết trực tiếp vào ứng dụng | Cần coordinator, hỗ trợ XA | Relay đảm nhận liên kết |
| Độ trễ phân phối | Thử ngay lập tức | Thời gian điều phối commit | Độ trễ bất đồng bộ |
| Xử lý trùng lặp | Tùy hiện thực | Vẫn phải xem xét riêng sau commit | Bắt buộc consumer lũy đẳng |
| Cô lập sự cố | Luồng yêu cầu bị ảnh hưởng | Sự cố của bên tham gia chặn giao dịch | Cô lập bằng tồn đọng relay |
| Gánh nặng vận hành | Ban đầu thấp, cao khi sự cố | Giao thức, khóa, khôi phục phức tạp | Vận hành outbox, relay, replay |
Outbox không phải lúc nào cũng vượt trội hơn 2PC. Nếu tính nguyên tử mạnh là bắt buộc trên nhiều kho dữ liệu và mọi bên tham gia đều hỗ trợ XA đã được kiểm chứng, có thể xem xét 2PC. Tuy nhiên, khi broker bên ngoài, SaaS hay dịch vụ đám mây không tham gia XA, hoặc khi tính sẵn sàng cao và liên kết lỏng là quan trọng, tính nhất quán cuối cùng (eventual consistency) của outbox là lựa chọn thực tế hơn.
5.2 Quan hệ với CDC, Event Sourcing và Saga
CDC là cơ chế truyền tải đọc và chuyển các thay đổi, còn outbox là mẫu ghi tường minh sự thật nghiệp vụ nào sẽ được công khai. Nếu công khai mọi row change của bảng nghiệp vụ ra bên ngoài, lược đồ nội bộ có thể bị đóng cứng thành hợp đồng sự kiện, vì vậy cách dùng CDC đọc các sự kiện outbox riêng biệt giúp ranh giới rõ ràng hơn.
Event Sourcing là mô hình lưu sự kiện như sự thật gốc của hệ thống và tái tạo trạng thái bằng cách phát lại các sự kiện đó. Outbox nhằm bảo đảm phát hành ra ngoài trong khi vẫn duy trì bảng trạng thái hiện tại thông thường, nên có thể dùng mà không cần Event Sourcing. Trong hệ thống đã áp dụng Event Sourcing, có thể thiết kế ranh giới phân phối giữa event store và broker bên ngoài bằng outbox hoặc relay dựa trên log.
Saga là mẫu điều phối nghiệp vụ trải qua nhiều dịch vụ bằng chuỗi giao dịch cục bộ và tác vụ bù trừ. Có thể dùng kèm outbox để ở mỗi bước saga, thay đổi DB của chính dịch vụ và lệnh/sự kiện tiếp theo được phát hành an toàn. Outbox bổ sung tính nguyên tử cho việc chuyển thông điệp, còn saga đảm nhận tiến trình nghiệp vụ và chính sách bù trừ của nhiều dịch vụ, nên không được nhầm lẫn vai trò.
6. Tình huống áp dụng
6.1 Đơn hàng thương mại điện tử
Dịch vụ đơn hàng đổi trạng thái đơn sang PAID_PENDING và đồng thời ghi PaymentRequested vào outbox.
Khi thanh toán được phê duyệt, dịch vụ thanh toán ghi cùng lúc lịch sử xử lý và trạng thái thanh toán, rồi đưa PaymentApproved vào outbox của chính mình.
Dịch vụ tồn kho và giao hàng dùng cùng ID đơn hàng và ID sự kiện để ngăn giữ hàng trùng và xuất kho trùng.
Việc mất sự kiện phê duyệt thanh toán có thể khiến đơn hàng chờ vĩnh viễn, nên đặt cảnh báo cho pending oldest age. Khi thử lại thanh toán mà chỉ mất phản hồi mạng, phải truy vấn kết quả thanh toán bằng cùng idempotency key; nếu tùy tiện tạo yêu cầu phê duyệt mới có thể dẫn tới thanh toán hai lần. Người vận hành phải có khả năng đối soát trạng thái đơn hàng, thanh toán, tồn kho để thực hiện lệnh hiệu chỉnh.
6.2 Chuyển khoản tài chính và sự kiện sổ cái
Dịch vụ tài khoản ghi số dư sổ cái và sự kiện chuyển khoản trong cùng một giao dịch cục bộ. Sự kiện chứa ID giao dịch, định danh tài khoản, số tiền, loại tiền tệ, sequence sổ cái nhưng loại trừ thông tin xác thực và dữ liệu cá nhân không cần thiết. Consumer dùng ràng buộc duy nhất của ID giao dịch và kiểm tra sequence để không phản ánh cùng một lần chuyển khoản hai lần và cách ly các sự kiện bị đảo thứ tự.
Trong nghiệp vụ tài chính, bất biến của sổ cái và khả năng đối soát quan trọng hơn cách nói "chỉ xử lý một lần". Cho phép broker gửi lại nhưng thiết kế ràng buộc lưu trữ và lịch sử xử lý để kết quả sổ cái cuối cùng chỉ được phản ánh một lần. Do yêu cầu lưu giữ, kiểm toán và kiểm soát truy cập, chính sách archive và xóa outbox phải được quyết định cùng bộ phận pháp chế và bảo mật.
6.3 Logistics và thay đổi trạng thái IoT
Trung tâm logistics lưu trạng thái thiết bị và phát hành sự kiện DeviceStateChanged để cập nhật các dịch vụ giám sát, bảo trì, thông báo.
Vì thứ tự trạng thái của cùng một thiết bị là quan trọng, dùng device ID làm khóa phân vùng, và nếu sequence nhỏ hơn hoặc bằng giá trị trước thì xử lý như sự kiện trùng/trễ.
Sự cố tạm thời ngắn được thử lại, còn trạng thái cũ của thiết bị ngoại tuyến lâu ngày được áp dụng chính sách ghi đè bằng trạng thái mới nhất hoặc loại bỏ.
Nếu số sự kiện mỗi giây lớn, cách polling từng hàng có thể gây gánh nặng cho DB. Khi đó kết hợp CDC với outbox phân vùng, relay theo lô và điều chỉnh chu kỳ lưu giữ, nhưng phải kiểm thử tải log cơ sở dữ liệu và thông lượng broker. Chỉ số vận hành không chỉ gồm độ trễ và mất mát theo từng thiết bị mà còn phải bao gồm độ tươi của màn hình giám sát do tồn đọng outbox.
7. Chuyên sâu: Chiến lược thiết kế, áp dụng và điểm dự kiến ra đề
7.1 Áp dụng theo giai đoạn
Ở giai đoạn đầu, chọn một use case cốt lõi cần sự kiện và tạo outbox với các trường tối thiểu trong cùng DB với bảng nghiệp vụ. Trước khi loại bỏ phát hành broker đồng bộ, có thể dùng chế độ shadow để quan sát so sánh phát hành hiện tại với phát hành qua outbox. Vì sự kiện trùng có thể đến consumer, trong giai đoạn chuyển đổi phải bảo đảm consumer idempotency trước tiên.
Ở giai đoạn hai, chuẩn hóa claim, lease, backoff, DLQ và xử lý lại của relay. Nếu mỗi dịch vụ tạo tên trạng thái và quy tắc thử lại khác nhau, người vận hành sẽ khó ứng phó sự cố, nên cần cung cấp thư viện chung và cơ chế bảo vệ ở mức nền tảng. Tuy nhiên, hợp đồng sự kiện có ý nghĩa khác nhau theo từng miền nên không ép buộc vào một mô hình payload chung khổng lồ.
Ở giai đoạn ba, mở rộng sang CDC hoặc nền tảng sự kiện, kết nối schema registry, contract test, lưu giữ và kiểm soát truy cập. Dù broker thay đổi, vẫn giữ nguyên ý nghĩa domain event và quy tắc message_id để giảm các thay đổi không cần thiết đối với hợp đồng của consumer. Hiệu quả áp dụng được đánh giá không chỉ bằng thông lượng đơn thuần mà bằng tỷ lệ mất sự kiện, độ trễ phân phối trung bình, tỷ lệ xử lý lại thành công và số trường hợp đối soát chưa giải quyết.
7.2 Cấu trúc bài làm dự kiến
Trong phần tổng quan của bài thi, trình bày bối cảnh ghi kép và giao dịch phân tán, rồi định nghĩa "lưu dữ liệu nghiệp vụ và outbox trong cùng một giao dịch cục bộ, relay phát hành bất đồng bộ". Sơ đồ khái niệm phải thể hiện đầy đủ bộ xử lý lệnh, giao dịch DB, bảng nghiệp vụ, outbox, relay, broker và idempotent consumer.
Trong phần thân, đối chiếu hai kịch bản thất bại của phát hành trực tiếp với việc ghi nguyên tử của outbox. Giải thích sự khác biệt giữa polling và CDC, thiết kế message_id, aggregate_id, sequence, cùng trùng lặp, thứ tự, thử lại, DLQ và chỉ số quan sát sẽ giúp bài làm thoát khỏi kiểu học thuộc đơn thuần.
Trong phần kết luận, nêu rõ giới hạn: outbox không hứa hẹn "exactly-once" mà cung cấp tính nguyên tử cục bộ và tính nhất quán cuối cùng có thể xử lý lại. Tùy mức độ quan trọng của nghiệp vụ, kết hợp với 2PC, Saga, Event Sourcing, và đề xuất cả consumer lũy đẳng, đối soát, bảo mật, chi phí, năng lực vận hành sẽ thể hiện được góc nhìn phán đoán của Kỹ sư chuyên nghiệp.
8. Những điểm cần cân nhắc và hàm ý
8.1 Nêu rõ phạm vi bảo đảm trong hợp đồng
Tài liệu hóa "bảo đảm phát hành sự kiện" bằng cách chia thành bảo đảm commit, bảo đảm phân phối tới broker và bảo đảm xử lý tiêu thụ. Mục tiêu mức dịch vụ bao gồm độ trễ phân phối tối đa, thời gian thử lại tối đa, thời gian xử lý DLQ và tỷ lệ trùng lặp chấp nhận được. Nếu người phụ trách nghiệp vụ không hiểu điều này, họ có thể hiển thị sai tính nhất quán cuối cùng bất đồng bộ cho người dùng.
8.2 Thiết kế tính lũy đẳng và thứ tự theo từng nghiệp vụ
Thay vì ép thứ tự toàn cục cho mọi thông điệp, chỉ định nghĩa thứ tự cần thiết theo từng aggregate. Kết hợp lịch sử xử lý của consumer, cập nhật có điều kiện, khóa duy nhất, idempotency key bên ngoài phù hợp với loại tác dụng phụ. Không chỉ ghi nhận trùng lặp như lỗi mà phải kiểm thử nó như một luồng phân phối lại bình thường.
8.3 Lưu trữ, lưu giữ và tối thiểu hóa dữ liệu cá nhân
Outbox giữ payload gốc cho đến khi sự kiện được phát hành, nên trở thành điểm sao chép thông tin nhạy cảm. Tối thiểu hóa các trường và thiết kế mã hóa, kiểm soát truy cập, che giấu (masking), thời hạn lưu giữ và lan truyền xóa. Ngay cả khi archive các sự kiện đã xử lý, cũng phải quyết định trước thứ tự ưu tiên giữa yêu cầu kiểm toán và yêu cầu xóa dữ liệu cá nhân.
8.4 An toàn vận hành và cô lập sự cố
Relay không được đặt khóa quá mức lên DB và phải cô lập để một hàng lỗi không chặn cả lô. Khi broker gặp sự cố, áp dụng backpressure, circuit breaker, rate limit, và đặt giới hạn trên cùng cảnh báo để tồn đọng outbox không làm cạn không gian lưu trữ của DB nghiệp vụ. Việc xử lý lại DLQ được kiểm soát bằng công cụ vận hành có quy trình phê duyệt, xem trước, dry-run, rollback hoặc bù trừ.
8.5 Tiến hóa hợp đồng và kiểm thử
Sự kiện không phải DTO của bảng nội bộ mà là hợp đồng mà consumer bên ngoài phụ thuộc vào, nên phải thận trọng khi xóa trường hay thay đổi ý nghĩa. Quản lý đồng thời schema version, quy tắc tương thích ngược, consumer-driven contract test và payload mẫu. Trong kiểm thử tích hợp, xác minh theo kịch bản: commit, rollback, relay gián đoạn, mất ACK từ broker, trùng lặp, đảo thứ tự và xử lý lại DLQ.
8.6 Cân bằng trong lựa chọn kiến trúc
Bảng outbox và relay đòi hỏi thêm thành phần lưu trữ và vận hành, nên áp dụng vô điều kiện cho dịch vụ CRUD đơn giản sẽ làm tăng chi phí. Ngược lại, nếu thay đổi dữ liệu dẫn tới thanh toán, tồn kho, quyền hạn, kiểm toán của các dịch vụ khác và chi phí mất sự kiện lớn, thì đáng để chấp nhận độ trễ phân phối nhỏ và độ phức tạp vận hành. Kỹ sư chuyên nghiệp phải lựa chọn giữa phát hành trực tiếp, outbox, 2PC và Saga dựa trên ranh giới giao dịch, chi phí thất bại, thông lượng, quy định và năng lực tổ chức.
Tài liệu tham khảo
- AWS Prescriptive Guidance, Transactional outbox pattern
- Chris Richardson, Pattern: Transactional outbox
- Debezium Documentation, Outbox Event Router
Tóm tắt một câu: Mẫu thiết kế ghi dữ liệu nghiệp vụ và sự kiện vào outbox trong cùng một giao dịch cục bộ, rồi chuyển đi với tính nhất quán cuối cùng thông qua relay có cơ chế consumer lũy đẳng, thứ tự và xử lý lại.