Sự An Toàn Giả Tạo Của Kiến Trúc Monolith
Chuyển từ một hệ thống monolith dùng chung cơ sở dữ liệu sang một cụm microservices gồm 15 dịch vụ giống như một bước nâng cấp khổng lồ, cho đến khi tôi đối mặt với thực tế về tính nhất quán dữ liệu. Trong monolith, tôi dựa vào các transaction ACID. Tôi có thể gói gọn mười lời gọi database khác nhau trong một khối BEGIN/COMMIT. Nếu server gặp sự cố giữa chừng, database sẽ tự xử lý rollback. Mọi thứ hoạt động hoàn hảo.
Trong microservices, mạng lưới an toàn đó không còn nữa. Order service, Payment service và Inventory service của bạn có thể sử dụng các database khác nhau—thậm chí là sự kết hợp giữa PostgreSQL và MongoDB. Việc cố gắng thực hiện một transaction toàn cục trên các node này bằng Two-Phase Commit (2PC) thường dẫn đến một hệ thống chậm chạp, dễ gãy ngay khi một request mạng bị trễ. Đó là lý do tại sao tôi tin dùng Saga pattern.
Cuộc Đua Tiếp Sức: Giải Mã Cấu Trúc Của Một Saga
Hãy coi Saga như một cuộc đua tiếp sức của các transaction cục bộ. Mỗi dịch vụ thực hiện công việc riêng, cập nhật database nội bộ, sau đó ra hiệu cho dịch vụ tiếp theo tiếp quản. Nếu một dịch vụ thất bại—chẳng hạn vì thẻ tín dụng bị từ chối hoặc kho hàng hết sạch—Saga sẽ kích hoạt một chuỗi “compensating transactions” (giao dịch bù đắp) để hoàn tác các bước trước đó.
Tôi thường chọn giữa hai phong cách triển khai tùy thuộc vào độ phức tạp của quy trình:
1. Choreography: Điệu Nhảy Phi Tập Trung
Với những luồng đơn giản chỉ có 2 hoặc 3 bước, tôi ưu tiên choreography. Không có “ông chủ” trung tâm nào cả. Mỗi dịch vụ phát ra một event, và các dịch vụ khác phản hồi lại. Nó nhẹ nhàng nhưng có thể nhanh chóng biến thành “đống spaghetti event” nếu bạn không cẩn thận.
- Order Service: Lưu order ở trạng thái ‘Pending’ và phát đi
OrderCreated. - Payment Service: Nhận event, xử lý thanh toán $49.99 và phát đi
PaymentSuccessful. - Inventory Service: Giữ 1 đơn vị SKU-101 và phát đi
InventoryReserved.
2. Orchestration: Nhạc Trưởng Trung Tâm
Khi một quy trình nghiệp vụ liên quan đến 5 dịch vụ trở lên, tôi chuyển sang Orchestrator. Đây là một state machine tập trung, chỉ định rõ ràng cho từng dịch vụ phải làm gì. Nó giúp việc debug dễ dàng hơn nhiều vì toàn bộ trạng thái của một giao dịch 5.000 USD có thể quan sát được tại một nơi duy nhất.
# Logic của Orchestrator trong Python
class OrderSagaOrchestrator:
def execute(self, order_id, amount):
try:
# Bước 1: Trừ tiền người dùng
payment_ref = payment_api.charge(amount)
# Bước 2: Giữ hàng trong kho
inventory_api.reserve(order_id)
# Bước 3: Hoàn tất
order_db.mark_as_paid(order_id)
except Exception as error:
self.rollback(order_id, payment_ref)
def rollback(self, order_id, payment_ref):
payment_api.refund(payment_ref)
order_db.cancel(order_id)
Bí Quyết Nằm Ở Đây: Compensating Transactions
Luồng thành công thì đơn giản. Luồng thất bại mới là nơi quyết định sự thành bại của Saga. Không giống như SQL rollback, một hành động bù đắp là một transaction mới nhằm đảo ngược logic của transaction trước đó. Nếu bạn đã gửi SMS xác nhận cho người dùng, bạn không thể “hủy gửi” nó; bạn phải gửi một SMS thứ hai giải thích về việc hủy đơn.
Sagas tuân theo nguyên tắc ACD. Chúng đảm bảo tính Nguyên tử (Atomicity), Nhất quán (Consistency) và Bền vững (Durability), nhưng thiếu tính Cách ly (Isolation). Điều này có means là khi một Saga đang chạy, các dịch vụ khác có thể nhìn thấy trạng thái trung gian “Pending”. Bạn phải thiết kế UI để xử lý việc này—ví dụ: hiển thị biểu tượng “Đang xử lý” thay vì dấu tích “Đã xác nhận” ngay lập tức.
Thiết Kế Nút “Hoàn Tác”
Tôi luôn đảm bảo mọi API endpoint đều có chiến lược đảo ngược tương ứng:
- Hành động:
ReserveStock(Trừ 5 đơn vị) -> Bù đắp:ReleaseStock(Cộng lại 5 đơn vị) - Hành động:
ApplyDiscount-> Bù đắp:RemoveDiscount - Hành động:
CreateShippingLabel-> Bù đắp:VoidShippingLabel
Việc quản lý mock data cho các luồng này có thể rất tẻ nhạt. Khi cần chuyển đổi các danh mục CSV lớn thành JSON để test microservices cục bộ, tôi sử dụng toolcraft.app/vi/tools/data/csv-to-json. Nó chạy trực tiếp trong trình duyệt, giúp giữ dữ liệu test nhạy cảm không bị đưa lên các server bên ngoài và đẩy nhanh tốc độ dev.
Tối Ưu Cho Môi Trường Production: Tính Idempotency và Độ Tin Cậy
Trong môi trường phân tán, lỗi mạng đồng nghĩa với việc các dịch vụ của bạn sẽ nhận được cùng một message hai lần. Nếu Inventory service xử lý event PaymentSuccessful hai lần, bạn sẽ vô tình trừ kho gấp đôi.
1. The Idempotency Key
Tôi không bao giờ xử lý một transaction mà không có mã định danh duy nhất (như UUID). Dịch vụ phải kiểm tra database: “Mình đã xử lý order_6789 chưa?” Nếu rồi, nó sẽ bỏ qua message trùng lặp và trả về kết quả thành công đã lưu trong cache.
2. The Transactional Outbox Pattern
Đừng bao giờ cập nhật database rồi mới cố gửi message tới RabbitMQ trong hai bước tách biệt. Nếu message broker bị sập, database của bạn sẽ bị lệch pha với phần còn lại của hệ thống. Thay vào đó, tôi lưu message vào một bảng outbox trong cùng transaction cục bộ với dữ liệu nghiệp vụ. Một background worker sau đó sẽ đẩy các message đó tới broker một cách tin cậy.
Những Bài Học Xương Máu Từ Thực Tế
Sau khi scale các Saga cho hệ thống xử lý hàng nghìn request đồng thời, đây là những đúc kết quan trọng nhất của tôi:
- Observability là bắt buộc: Gắn một
correlation_idvào mọi log. Nếu một đơn hàng bị kẹt trong 120 giây, bạn cần thấy chính xác chuỗi xử lý bị đứt ở đâu trên log của năm dịch vụ khác nhau. - Giữ Transaction ngắn gọn: Vì thiếu tính isolation, các transaction chạy lâu sẽ làm tăng nguy cơ race condition. Hãy hướng tới các transaction cục bộ hoàn tất trong dưới 200ms.
- Thiết lập Timeout nghiêm ngặt: Nếu Payment gateway không phản hồi trong 10 giây, đừng chờ đợi mãi mãi. Hãy kích hoạt luồng bù đắp tự động để giải phóng hàng trong kho.
- Tránh phụ thuộc vòng quanh: Trong choreography, hãy đảm bảo Service A không đợi Service B, trong khi B lại đang đợi A. Bạn sẽ kết thúc with một distributed deadlock.
Saga phức tạp hơn các transaction SQL tiêu chuẩn. Tuy nhiên, đó là cách duy nhất tôi tìm thấy để xây dựng một kiến trúc đa database bền vững mà không bị tham nhũng dữ liệu (data corruption). Hãy bắt đầu với một luồng 2 bước nhỏ, nắm vững logic bù đắp và luôn giả định rằng mạng sẽ gặp sự cố.

