Quy trình nhiều bước
Giữ dữ liệu nhất quán khi một nghiệp vụ đi qua nhiều service: saga choreography và orchestration, hành động bù trừ, trạng thái PENDING, transactional outbox, idempotent consumer và workflow engine như Temporal.
Bài toán: một nghiệp vụ, nhiều service, không có transaction chung
Người dùng bấm "Đặt hàng". Phía sau, hệ thống phải:
- Tạo đơn trong Order service.
- Trừ tiền qua Payment service, service này lại gọi cổng thanh toán bên ngoài.
- Giữ hàng trong Inventory service.
- Tạo vận đơn trong Shipping service.
Mỗi service có database riêng. Không có BEGIN ... COMMIT nào bao được cả bốn: Building Microservices ch.5 mô tả đúng tình huống này, khi tách schema ta mất sự an toàn của một transaction duy nhất, và câu hỏi là điều gì xảy ra khi bước này ghi được mà bước kia thất bại.1 Bước 3 có thể thất bại (hết hàng) sau khi bước 2 đã trừ tiền. Process có thể chết giữa bước 2 và 3. Mạng có thể timeout, bạn không biết cổng thanh toán đã trừ tiền hay chưa.
Pattern này trả lời câu hỏi: làm sao để quy trình hoặc đi tới cuối, hoặc quay về trạng thái hợp lệ, kể cả khi bất kỳ bước nào hỏng, ở bất kỳ thời điểm nào.
Các tình huống giống vậy: đặt tour (vé máy bay + khách sạn + xe), chuyển tiền giữa hai ngân hàng, onboarding tài khoản (tạo user, cấp quyền, gửi email, tạo ví), xử lý video (upload, transcode, tạo thumbnail, publish).
Cách ngây thơ: gọi lần lượt, hoặc dùng 2PC
Gọi HTTP lần lượt trong một handler
Building Microservices ch.5 liệt kê các lựa chọn khi một thao tác bị chia qua nhiều ranh giới transaction: thử lại sau (eventual consistency), huỷ toàn bộ thao tác bằng compensating transaction, hoặc dùng distributed transaction. Sách cũng cảnh báo khi phải bù trừ cho ba, bốn, năm thao tác thì việc xử lý từng kiểu lỗi trở nên rất khó hiểu và khó cài đặt, và compensating transaction cũng có thể thất bại.2
app.post("/orders", async (req, res) => {
const order = await orders.create(req.body);
await payment.charge(order.id, order.total); // ok
await inventory.reserve(order.items); // lỗi: hết hàng
await shipping.create(order.id);
res.json(order);
});
Những gì có thể sai:
inventory.reservelỗi: tiền đã trừ, đơn nằm đó, không ai hoàn tiền.- Process bị kill sau
charge: không có gì ghi lại rằng quy trình đang dở ở đâu. Không ai tiếp tục, không ai bù trừ. chargetimeout: retry thì có thể trừ tiền hai lần, không retry thì có thể đơn đã trả tiền mà bị coi là chưa. Building Microservices ch.11 nói thao tác idempotent cho phép lặp lại lời gọi nhiều lần mà không gây hại, hữu ích khi phát lại message chưa chắc đã xử lý.3- Request HTTP của người dùng phải chờ cả chuỗi, có thể vài giây.
Two-phase commit (2PC)
2PC (XA transaction) cho một coordinator (transaction manager) hỏi mọi bên bỏ phiếu "sẵn sàng chưa", rồi ra lệnh commit đồng loạt; một phiếu "không" là đủ để rollback tất cả.4 Về lý thuyết nó cho atomicity thật. Trong thực tế:
- Các bên phải dừng chờ coordinator. Coordinator chết thì các transaction đang chờ không bao giờ hoàn tất, một bên không trả lời khi bỏ phiếu thì mọi thứ bị chặn, và các transaction chờ có thể giữ khóa gây tranh chấp.4
- Nhiều hệ thống hiện đại không hỗ trợ: Microservices Patterns nêu MongoDB, Cassandra, RabbitMQ và Kafka không hỗ trợ distributed transaction, cổng thanh toán bên ngoài cũng vậy (nhận định của khoá học).5
- Availability của cả hệ thống bằng tích availability từng phần: hai service 99,5% cho 99% chung.5
Microservices Patterns ch.4 kết luận rằng distributed transaction không phải công nghệ khả thi cho ứng dụng hiện đại, hãy dùng saga.5
Saga: chuỗi transaction cục bộ có bù trừ
Saga là một dãy transaction cục bộ, mỗi transaction nằm gọn trong một service và commit ngay. Nếu bước thứ k thất bại, saga chạy hành động bù trừ (compensating transaction) cho các bước 1 tới k-1 theo thứ tự ngược lại: dãy thực thi là T1 ... Tn, Cn ... C1.6
| Bước | Hành động | Bù trừ |
|---|---|---|
| 1 | Tạo đơn trạng thái PENDING | Chuyển đơn sang CANCELLED |
| 2 | Trừ tiền | Hoàn tiền |
| 3 | Giữ hàng | Trả hàng về kho |
| 4 | Tạo vận đơn | (không cần, đây là bước cuối) |
| 5 | Đơn sang APPROVED |
Ba loại bước, theo cách phân loại của Microservices Patterns:
- Compensatable: có thể bù trừ (bước 1, 2, 3).
- Pivot: bước "điểm không quay lại" (go/no-go). Nếu pivot commit thì saga chạy tới hết.
- Retriable: các bước sau pivot, được bảo đảm hoàn tất nếu cứ thử lại (gửi email xác nhận, tạo vận đơn).7
Mẹo thiết kế (của khoá học): đặt bước dễ thất bại nhất (kiểm tra tồn kho, kiểm tra hạn mức) trước bước khó bù trừ (trừ tiền thật). Hoàn tiền tốn phí và làm khách khó chịu, nên giữ hàng trước rồi mới trừ tiền thường tốt hơn.
Lưu ý: Bù trừ khác rollback. Building Microservices cũng nói compensating transaction là một transaction mới khởi động để "quay lại" điều vừa xảy ra, và nó cũng có thể thất bại.2 Tiền đã trừ thì khách đã thấy trên app ngân hàng. Hoàn tiền là một giao dịch mới, có thể mất vài ngày (ví dụ của khoá học). Email đã gửi thì không thu hồi được. Hãy thiết kế để những bước không thể bù trừ nằm sau pivot.7
Choreography: các service tự nói chuyện qua event
Không có ai chỉ huy. Mỗi service lắng nghe event và phát event mới: Microservices Patterns §4.2.1 mô tả choreography là không có coordinator trung tâm, các participant đăng ký sự kiện của nhau và phản ứng tương ứng.8
- Ưu: đơn giản, các service lỏng lẻo với nhau (không biết trực tiếp về nhau). Sách nhận xét choreography chạy tốt cho saga đơn giản.9
- Nhược: khó hiểu hơn vì không có một chỗ trong code định nghĩa saga, tạo phụ thuộc vòng giữa các service (một "design smell"), và có nguy cơ coupling chặt vì mỗi participant phải đăng ký mọi event liên quan.9 Việc ghép log từ nhiều service để biết "đơn 123 đang ở đâu" là hệ quả thực tế (nhận định của khoá học).
Orchestration: một bộ điều phối ra lệnh
Một orchestrator (thường nằm trong Order service) giữ trạng thái saga và gửi lệnh tới từng service, chờ trả lời, quyết định bước tiếp hoặc bù trừ. Microservices Patterns §4.2.2 mô tả orchestrator là một lớp chỉ có trách nhiệm bảo các participant làm gì, giao tiếp theo kiểu command và reply bất đồng bộ.10
- Ưu: theo sách, orchestration không tạo phụ thuộc vòng, ít coupling hơn, và tách mối quan tâm tốt hơn vì logic điều phối nằm một chỗ.11 Việc "dễ test, dễ trả lời đang ở bước nào" là nhận định của khoá học.
- Nhược: orchestrator có thể phình to thành nơi chứa logic nghiệp vụ của service khác. Sách khuyên thiết kế orchestrator chỉ chịu trách nhiệm sắp xếp thứ tự, logic cụ thể vẫn ở từng service, và khuyên dùng orchestration cho mọi saga trừ loại đơn giản nhất.11
| Choreography | Orchestration | |
|---|---|---|
| Số bước hợp lý (khoá học) | 2–3 | 3 trở lên |
| Nơi chứa logic quy trình | rải ở các service | một orchestrator |
| Quan sát trạng thái | khó | dễ, có bảng saga state |
| Coupling | qua event | orchestrator biết mọi bên |
Trong phỏng vấn: Với quy trình 4 bước có tiền bạc, chọn orchestration và giải thích bằng khả năng quan sát và bù trừ. Nhắc choreography như phương án cho luồng phụ, ví dụ gửi email và cập nhật analytics khi đơn đã APPROVED.
Thiếu isolation: semantic lock và trạng thái PENDING
Saga có ACD nhưng không có I. Các transaction cục bộ commit ngay, nên service khác có thể đọc thấy trạng thái dở dang. Microservices Patterns §4.3 liệt kê các bất thường: lost update, dirty read, và đưa ra các countermeasure.12
Countermeasure hay dùng nhất là semantic lock: bản ghi mang trạng thái *_PENDING để báo "đang có saga xử lý". Sách dùng đúng ví dụ này: Order ở APPROVAL_PENDING báo cho các saga khác biết có saga đang cập nhật, cờ này được xoá khi saga thành công hoặc bị bù trừ.13
- Đơn mới tạo ở
APPROVAL_PENDING. UI hiện "Đang xử lý". Không cho hủy hay sửa trong trạng thái này, hoặc cho phép nhưng đưa yêu cầu vào hàng đợi. - Inventory giữ hàng bằng một bản ghi reservation, không trừ thẳng vào số tồn khả dụng cho tới khi saga thành công.
- Saga kết thúc mới chuyển sang
APPROVEDhoặcCANCELLED.
Các countermeasure khác:
- Commutative update: thiết kế thao tác có thể đổi thứ tự (debit/credit của một Account), giúp loại bỏ lost update.14
- Reread value: trước khi ghi, đọc lại để chắc dữ liệu chưa đổi (một dạng optimistic locking, xem Tranh chấp tài nguyên).14
- By value: chọn cơ chế theo rủi ro nghiệp vụ: yêu cầu rủi ro thấp chạy bằng saga, yêu cầu rủi ro cao (số tiền lớn) có thể dùng distributed transaction.14
Gửi event đáng tin cậy: transactional outbox và idempotent consumer
Saga chạy trên message. Hai lỗi kinh điển làm hỏng nó: ghi DB xong nhưng gửi message thất bại, và message tới hai lần.
Transactional outbox
// sai: hai hệ thống, không có transaction chung
await db.query("UPDATE orders SET state = 'APPROVAL_PENDING' WHERE id = $1", [id]);
await kafka.send({ topic: "orders", messages: [{ key: id, value: evt }] }); // process chết ở đây?
Microservices Patterns §3.3.7 đưa ra cách sửa (Transactional outbox): dùng một bảng trong database làm hàng đợi tạm, ghi event vào bảng outbox trong cùng transaction với thay đổi nghiệp vụ. Một tiến trình riêng (message relay) đọc outbox và publish sang broker.15 Sách giải thích vì sao cần: nếu service cập nhật DB rồi crash trước khi gửi message, hệ thống sẽ bất nhất, và distributed transaction không dùng được với nhiều broker như Kafka.15
BEGIN;
UPDATE orders SET state = 'APPROVAL_PENDING' WHERE id = 'o-123';
INSERT INTO outbox (id, aggregate_id, type, payload)
VALUES (gen_random_uuid(), 'o-123', 'OrderCreated', '{"total": 450000}');
COMMIT;
Relay có hai cách:
- Polling publisher: mỗi vài trăm ms
SELECT ... FROM outbox WHERE published = false ORDER BY id LIMIT 100, gửi, đánh dấu đã gửi. Sách nói polling đơn giản và chạy khá tốt ở quy mô thấp, nhưng polling DB thường xuyên có thể tốn kém.16 Con số "tới vài nghìn event/s" là ước lượng của khoá học.17 - Transaction log tailing: đọc transaction log của DB (WAL của Postgres, binlog MySQL) và publish các thay đổi; sách coi đây là cách tinh vi và hiệu năng cao hơn polling.16 Việc dùng công cụ CDC như Debezium và chi tiết độ trễ chưa có sách trong thư viện.18 Xem Change Data Capture.
Relay có thể gửi trùng (gửi xong, chết trước khi đánh dấu). Outbox cho at-least-once, không cho exactly-once (suy luận từ cơ chế trên). Phía nhận phải chịu được trùng.19
Idempotent consumer
Microservices Patterns §3.3.6 đề xuất: nếu logic không idempotent, consumer ghi lại ID của message đã xử lý (ví dụ trong bảng riêng, INSERT thất bại với message trùng) và loại bỏ message trùng, trong cùng transaction với thay đổi nghiệp vụ:19
BEGIN;
INSERT INTO processed_messages (consumer, message_id) VALUES ('inventory', 'm-789');
-- nếu vi phạm khóa chính: message đã xử lý, ROLLBACK và ack luôn
UPDATE stock SET reserved = reserved + 2 WHERE sku = 'IP15-128';
COMMIT;
Với lời gọi ra ngoài (cổng thanh toán), gửi kèm idempotency key cố định cho mỗi bước của saga, ví dụ charge:o-123. Cổng thanh toán thấy key cũ thì trả lại kết quả cũ thay vì trừ tiền lần nữa. Đây là cách giải quyết tình huống timeout không rõ kết quả: cứ retry với cùng key. Ý nền tảng (thao tác idempotent an toàn khi lặp lại) có trong Building Microservices ch.11;3 cơ chế idempotency key cụ thể của từng cổng thanh toán chưa có sách trong thư viện.20
Workflow engine: Temporal
Phần này chưa có sách trong thư viện: thư viện không có sách về Temporal hay workflow engine; chỉ nêu khái niệm cơ bản, chi tiết ở bài Temporal.21 Tự viết orchestrator nghĩa là tự làm bảng saga state, timer, retry có backoff, xử lý timeout, phiên bản hóa quy trình, giao diện xem trạng thái. Workflow engine làm sẵn những thứ đó. Temporal là lựa chọn phổ biến: bạn viết quy trình như code tuần tự bình thường, engine ghi lại lịch sử từng bước và tự chạy tiếp từ chỗ dừng khi worker chết.
// Temporal workflow (TypeScript SDK), rút gọn
export async function orderSaga(order: Order): Promise<void> {
const compensations: Array<() => Promise<void>> = [];
try {
await reserveStock(order); compensations.unshift(() => releaseStock(order));
await chargePayment(order); compensations.unshift(() => refundPayment(order));
await createShipment(order);
await markApproved(order.id);
} catch (err) {
for (const undo of compensations) await undo();
await markCancelled(order.id);
throw err;
}
}
Mỗi hàm reserveStock, chargePayment là một activity: được retry tự động theo policy và phải idempotent. Workflow có thể sleep 3 ngày hoặc chờ tín hiệu từ người dùng mà không giữ thread nào.
| Cách làm | Hợp khi | Chi phí |
|---|---|---|
| Choreography qua event | 2–3 bước, đội nhỏ | khó quan sát |
| Orchestrator tự viết + outbox | vài quy trình, muốn kiểm soát | tự làm retry, timer, UI |
| Temporal hoặc engine tương tự | nhiều quy trình dài, có chờ người, có timer | thêm một cụm hạ tầng cần vận hành hoặc trả tiền dịch vụ |
Ví dụ hoàn chỉnh: đơn hàng → thanh toán → kho → giao hàng
Ví dụ này là cách ghép các kỹ thuật ở trên của khoá học (saga orchestration, semantic lock, outbox, idempotency); các mốc thời gian như 15 phút hay 30 phút là giả định.17 Nền tảng của các bước: saga và bù trừ,6 semantic lock,13 outbox15 và xử lý message trùng.19
POST /orderstạo đơnAPPROVAL_PENDINGvà insertOrderCreatedvào outbox trong một transaction. Trả về202 AcceptedvớiorderId. Client theo dõi trạng thái qua polling hoặc push.- Orchestrator nhận event, gửi lệnh
ReserveStockvới idempotency keyreserve:o-123. Inventory tạo reservation hết hạn sau 15 phút. - Hết hàng: orchestrator chuyển đơn sang
CANCELLED, xong. Chưa trừ tiền nên không có gì phải hoàn. - Đủ hàng: gửi
ChargePaymentvới keycharge:o-123. Timeout thì retry cùng key. Thẻ bị từ chối: chạyReleaseStock, đơnCANCELLED. - Thanh toán thành công là pivot. Từ đây mọi bước chỉ retry, không bù trừ: xác nhận reservation, tạo vận đơn, đơn
APPROVED, gửi email qua event. - Một job quét các saga kẹt quá 30 phút ở cùng một bước và cảnh báo. Lỗi không tự khỏi thì cần người xử lý tay.
Scale theo quy mô
Các mốc người dùng và con số dưới đây là giả định của khoá học, không phải số trong sách.17
~10k user. Không cần microservices. Một monolith với một Postgres, quy trình nằm trong một transaction thật. Chỉ có cổng thanh toán là bên ngoài: dùng idempotency key và một bảng trạng thái thanh toán là đủ. Nếu cần gửi event, polling outbox mỗi giây.
~1M user. Giả sử 50.000 đơn/ngày, đỉnh ×10 thì khoảng 6 đơn/s (ước tính). Mỗi đơn khoảng 10 message giữa các service, tức 60 message/s. Con số nhỏ. Thách thức là độ đúng và khả năng vận hành: orchestration, outbox polling, bảng processed_messages dọn sau 7 ngày, dashboard đếm saga theo trạng thái, cảnh báo saga kẹt.
50M+ user. Hàng triệu đơn mỗi ngày, flash sale tạo đỉnh ×50. Outbox chuyển sang CDC để không tạo thêm tải query. Kafka partition theo orderId để mọi event của một đơn giữ thứ tự: đúng cơ chế sharded channel mà Microservices Patterns §3.3.5 mô tả, mỗi shard chỉ giao cho một receiver trong nhóm.22 Orchestrator chạy nhiều instance, mỗi saga được một instance nhận theo partition. Cân nhắc Temporal hoặc engine tương tự khi có hàng chục loại quy trình. Kiểm tra định kỳ (reconciliation) đối chiếu đơn, giao dịch thanh toán và tồn kho cuối ngày, vì ở quy mô này lỗi hiếm cũng xảy ra hằng ngày. Building Microservices cũng nhắc khi compensating transaction thất bại thì cần retry hoặc một tiến trình nền dọn bất nhất về sau.2
Sai lầm hay gặp: Gọi saga xong là nghĩ dữ liệu luôn nhất quán. Saga cho eventual consistency (các bước commit cục bộ ngay, thiếu isolation).12 Phải thiết kế UI cho trạng thái trung gian ("Đang xác nhận thanh toán") và có cơ chế đối soát.
Bài nào dùng pattern này
- Hệ thống thanh toán: idempotency key, trạng thái PENDING, đối soát với ngân hàng.
- Đặt vé sự kiện: giữ chỗ, thanh toán, xuất vé, hoàn tiền khi hết hạn giữ chỗ.
- Flash sale: giữ hàng, thanh toán, hủy đơn quá hạn.
- Giao hàng nội thành và Gọi xe: chuyến đi là một quy trình dài nhiều trạng thái.
- YouTube: upload, transcode, publish là workflow nhiều bước có retry.
- Lập lịch tác vụ: chạy và theo dõi job nhiều bước.
Danh sách trên là gợi ý ghép bài của khoá học; nền tảng saga, outbox và xử lý trùng có nguồn ở các phần trên.615
Đọc thêm: Temporal, Kafka, Change Data Capture, Tác vụ dài.
Nguồn trích dẫn
-
Building Microservices (Sam Newman) (file .zip, tải về giải nén) · ch.5 Splitting the Monolith · mục "Transactional Boundaries" (tr. 89–91). ↩
-
Building Microservices (file .zip, tải về giải nén) · ch.5 · mục "Transactional Boundaries" › "Try Again Later" và "Abort the Entire Operation" (tr. 91–92). ↩ ↩2 ↩3
-
Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Idempotency" (tr. 215–216). ↩ ↩2
-
Building Microservices (file .zip, tải về giải nén) · ch.5 · mục "Transactional Boundaries" › "Distributed Transactions" (tr. 92–93). ↩ ↩2
-
Microservices Patterns (Chris Richardson) (file .zip, tải về giải nén) · ch.4 Managing transactions with sagas · §4.1 Transaction management in a microservice architecture › §4.1.2 The trouble with distributed transactions (tr. 112–114). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (tr. 114–117; dãy T1...Tn, Cn...C1). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.3.2 Countermeasures for handling the lack of isolation (định nghĩa compensatable, pivot, retriable transaction, tr. 128–129); và §4.1.3. ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.2.1 Choreography-based sagas (tr. 118–121). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.2.1 › "Benefits and drawbacks of choreography-based sagas" (tr. 121). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.2.2 Orchestration-based sagas (tr. 121–126). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.2.2 › "Benefits and drawbacks of orchestration-based sagas" (tr. 125–126). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.3 Handling the lack of isolation › §4.3.1 Overview of anomalies (tr. 126–128). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.3.2 › "Countermeasure: Semantic lock" (tr. 129–131). ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.4 · §4.3.2 › "Countermeasure: Commutative updates", "Reread value" và "By value" (tr. 131–132). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.7 Transactional messaging › "Using a database table as a message queue" (tr. 98–100). ↩ ↩2 ↩3 ↩4
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.7 › "Publishing events by using the Polling publisher pattern" và "Publishing events by applying the Transaction log tailing pattern" (tr. 99–101). ↩ ↩2
-
Quy tắc ước lượng của khoá học, xem bài Chọn server. Không phải số liệu trích từ sách; cần load test với ứng dụng thật. ↩ ↩2 ↩3
-
Chưa có nguồn trong thư viện: Debezium và các công cụ CDC cụ thể không có trong sách của thư viện; chỉ nêu ý cơ bản. ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages › "Writing idempotent message handlers" và "Tracking messages and discarding duplicates" (tr. 95–97). ↩ ↩2 ↩3
-
Chưa có nguồn trong thư viện: idempotency key của cổng thanh toán là đặc thù từng nhà cung cấp, sách trong thư viện không đề cập. ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về Temporal hoặc workflow engine; phần này chỉ nêu khái niệm cơ bản, không đưa số liệu cụ thể của sản phẩm. ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.5 Competing receivers and message ordering (tr. 94–95). ↩