99 Ngày Spring — Ngày 80: Saga và Tính nhất quán Dữ liệu
Order, inventory và payment giữ cơ sở dữ liệu riêng; một transaction ACID không bao trùm cả ba. Nếu lưu đơn xong mà phát sự kiện thất bại, dữ liệu lệch nhau. Transactional outbox giải bài toán ghi dữ liệu cùng sự kiện; saga điều phối nhiều local transaction với các bước bù; idempotency xử lý giao lại message.

1. Dual write và transactional outbox
Gọi database rồi gọi broker là hai thao tác độc lập: lỗi giữa hai bước để lại một phía đã đổi. Ghi đơn và bản ghi outbox trong cùng một DB transaction. Một relay sau đó phát sự kiện và đánh dấu đã gửi. Relay có thể phát trùng nếu chết sau publish nhưng trước khi đánh dấu, vì thế consumer vẫn phải idempotent.
@Transactional
void place(Order order) {
orders.save(order);
outbox.save(new OutboxEvent(UUID.randomUUID(), "OrderPlaced", order.id()));
}
// Separate relay: read unsent rows -> publish -> mark sent.2. Saga: local transaction và bước bù
Saga gồm các bước cục bộ: tạo đơn, giữ hàng, thu tiền. Nếu thu tiền thất bại, phát lệnh hủy giữ hàng và đánh dấu đơn thất bại. Compensation là hành động nghiệp vụ mới, không phải rollback database xuyên service; nó cũng có thể thất bại và cần retry/quan sát. Thiết kế trạng thái trung gian PENDING để API không báo thành công quá sớm.
PENDING -> STOCK_RESERVED -> PAID
| charge fails
v
RELEASE_STOCK -> FAILED3. Choreography hay orchestration
Choreography để từng service phản ứng với sự kiện: ít trung tâm nhưng luồng dài dễ khó theo dõi. Orchestration dùng một coordinator giữ trạng thái và gửi command: đường đi rõ, nhưng coordinator phải bền vững và tránh ôm nghiệp vụ của mọi service. Chọn theo số bước, nhánh lỗi, nhu cầu quan sát và quyền sở hữu. Với luồng phức tạp, sơ đồ trạng thái và timeout là bắt buộc.
Ít bước, ít nhánh: choreography có thể đủ.
Nhiều bước bù và timeout: coordinator thường dễ kiểm soát hơn.
4. Idempotency và kiểm chứng lỗi
Mỗi event có eventId ổn định. Consumer lưu eventId vào bảng processed_message có unique constraint trong cùng transaction với side effect; thấy lại ID thì bỏ qua. Command bù cũng cần idempotent. Thử giết relay sau publish, gửi message trùng, làm payment timeout và làm compensation lỗi; kiểm tra hệ thống cuối cùng hội tụ về trạng thái hợp lệ và có cảnh báo nếu bị kẹt.
BEGIN;
INSERT INTO processed_message(consumer, event_id) VALUES ('billing', :id);
UPDATE invoice SET state = 'PAID' WHERE order_id = :order;
COMMIT;
-- Unique (consumer, event_id) rejects a duplicate safely.Outbox xử lý dual write; saga xử lý chuỗi giao dịch; idempotency xử lý giao lại. Ba mảnh cần đi cùng nhau. Ngày 81 chuyển sang WebFlux và luồng reactive.
Tài liệu đối chiếu
https://microservices.io/patterns/data/transactional-outbox.html
https://microservices.io/patterns/data/saga.html
https://microservices.io/patterns/communication-style/idempotent-consumer.html
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.


