Backend

99 Ngày Spring — Ngày 80: Saga và Tính nhất quán Dữ liệu

SSite Admin
15 tháng 10, 2026 2 phút đọc 5 lượt xem
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.

Saga và Tính nhất quán Dữ liệu

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 -> FAILED

3. 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

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 Java — Ngày 80: Memory Leak trong Java

Rò rỉ do cache, listener, ThreadLocal và classloader; tìm retained size, GC roots rồi sửa nguồn giữ.

15 thg 10, 20262 phút9
99 Ngày Spring — Ngày 79: RabbitMQ với Spring

Exchange, queue, binding, @RabbitListener, retry/DLX và cách chọn RabbitMQ hay Kafka.

14 thg 10, 20262 phút6
99 Ngày Java — Ngày 79: JVM Flags và Tuning cơ bản

-Xms/-Xmx, chọn GC, GC log, heap dump và quy trình điều tra OutOfMemoryError.

14 thg 10, 20262 phút4