[Microservices Thực Chiến] Saga Pattern: Nghệ thuật "Quay xe" (Rollback) an toàn giữa chốn giang hồ phân tán
Chào anh em, lại là mình đây.
Ở bài viết trước về Eventual Consistency, chúng ta đã thống nhất với nhau rằng trong kiến trúc Microservices, để hệ thống gánh được tải cao, chúng ta phải chấp nhận sự "từ từ đồng bộ" thông qua Message Queue (như Kafka).
Nhưng cuộc đời đâu có dễ ăn thế. Giả sử anh em đang code một hệ thống backend bán mỹ phẩm. Luồng mua hàng là: Tạo đơn (Order) -> Trừ tiền ví (Payment) -> Trừ tồn kho (Inventory).

Chuyện gì sẽ xảy ra nếu Order tạo thành công, Payment trừ tiền khách thành công, nhưng chạy sang đến cái kho thì... ôi thôi, lọ serum cuối cùng vừa bị thằng khác múc mất rồi?
Lúc này, tiền thì đã trừ mà hàng thì không có. Khách hàng tế sống cả công ty lên ngay! Nếu ở kiến trúc Monolithic (nguyên khối) xài chung 1 cục Database, anh em chỉ cần gõ lệnh ROLLBACK một phát là xong. Order chạy Node.js có DB riêng, Inventory chạy Golang có DB riêng. Anh em không thể dùng lệnh Rollback của MySQL chéo qua 2 server khác nhau được. Để giải quyết bài toán "vỡ kế hoạch" này, các pháp sư kiến trúc phần mềm đã đẻ ra một thứ gọi là Saga Pattern.
1. Saga Pattern rốt cuộc là cái quái gì?
Saga không phải là một công nghệ hay thư viện, nó là một Design Pattern (Mẫu thiết kế) dành riêng cho việc xử lý các giao dịch phân tán (Distributed Transactions).

Bản chất của Saga rất đơn giản: Nó chia một cái giao dịch to đùng thành một chuỗi các giao dịch nhỏ (Local Transactions).
Mỗi giao dịch nhỏ sẽ cập nhật dữ liệu trong chính cái Database của nó, sau đó nó bắn ra một cái Event (sự kiện) để kích hoạt thằng Service tiếp theo làm việc.
Linh hồn của Saga: Giao dịch bù trừ (Compensating Transaction)
Điểm ăn tiền nhất của Saga nằm ở đây. Vì không có lệnh ROLLBACK tổng, nên bắt buộc MỖI một giao dịch nhỏ tiến lên phía trước, đều phải có sẵn một giao dịch lùi lại để dọn rác nếu có biến.
Áp dụng vào ví dụ mua mỹ phẩm ở trên, chúng ta sẽ thiết kế các Compensating Transactions như sau:
- Giao dịch Thuận:
Trừ tiền ví-> Giao dịch Bù trừ:Hoàn tiền ví (+ lại tiền). - Giao dịch Thuận:
Tạo đơn Pending-> Giao dịch Bù trừ:Đổi trạng thái đơn thành Canceled.
Luồng chạy thực tế khi bị lỗi kho sẽ trông như thế này:
- Order Service tạo đơn Pending.
- Payment Service trừ tiền ví (Thành công).
- Inventory Service kiểm tra kho -> Báo hết hàng (Thất bại).
- Thằng Inventory lập tức la lên (bắn event): "Lỗi rồi anh em ơi, quay xe!"
- Payment Service nghe thấy, lập tức chạy hàm Hoàn tiền cho khách.
- Order Service nghe thấy, lập tức chạy hàm Hủy đơn hàng.
Kết quả: Dữ liệu nhất quán trở lại, khách nhận lại tiền, hệ thống an toàn.
2. Hai trường phái múa võ trong Saga
Để quản lý cái chuỗi sự kiện dích dắc này, giang hồ chia làm 2 trường phái chính:
Trường phái 1: Choreography (Múa tập thể / Không cần nhạc trưởng) Ở kiểu này, các Services tự giao tiếp với nhau thông qua Message Queue (Kafka/RabbitMQ) mà không cần ai đứng ra quản lý cả.
- Cách hoạt động: Service A làm xong việc thì thả một cục Message vào Queue. Service B tự biết thân biết phận nhảy vào Queue nhặt Message đó ra làm tiếp. Lỗi ở đâu thì thằng đó thả Message "Báo lỗi" vào Queue để các thằng trước tự biết mà Undo.
- Ưu điểm: Code lỏng lẻo (Loosely coupled), không bị nút thắt cổ chai (Single point of failure).
- Nhược điểm: Khi quy mô lên tới 10-20 cái Services, việc trace bug (tìm lỗi) sẽ giống như đi vào mê cung. Anh em sẽ không biết cái luồng chạy nó đang tắc ở thằng nào.
Trường phái 2: Orchestration (Có Nhạc trưởng chỉ đạo)
Ở kiểu này, anh em phải code thêm một thằng Service riêng biệt, gọi là Orchestrator (Nhạc trưởng).
- Cách hoạt động: Mọi Service khác không nói chuyện trực tiếp với nhau, mà đều phải báo cáo cho Nhạc trưởng. Nhạc trưởng sẽ cầm một cái kịch bản (State Machine): Báo Order tạo đơn đi! Báo Payment trừ tiền đi! Payment trả về lỗi à? Báo Order hủy đơn đi!
- Ưu điểm: Cực kỳ dễ quản lý, dễ theo dõi trạng thái của toàn bộ luồng giao dịch.
- Nhược điểm: Thằng Nhạc trưởng bị ôm đồm quá nhiều việc. Nếu Nhạc trưởng sập, toàn bộ hệ thống tê liệt.
3. Kinh nghiệm xương máu khi chơi hệ Saga
Nếu anh em định áp dụng Saga vào dự án thực tế, hãy khắc cốt ghi tâm những điều sau để không bị ăn hành:
- Tính lũy đẳng (Idempotency): Trong môi trường phân tán, mạng có thể lag khiến một cái Event bị Kafka bắn lại 2 lần. Code hàm "Hoàn tiền" của anh em phải được thiết kế sao cho: Dù có gọi lệnh hoàn 100k đó 10 lần, thì tài khoản user cũng chỉ được cộng đúng 100k thôi, không là công ty phá sản đấy.
- Trace ID là sinh mạng: Khi các giao dịch nhảy múa giữa các server Node.js và Golang, anh em bắt buộc phải gắn một cái transaction_id (hoặc trace_id) duy nhất vào mỗi request. Để lúc log ra Kibana/Elasticsearch còn biết mà xâu chuỗi chúng nó lại với nhau.
- Giám sát (Monitoring): Saga rất dễ sinh ra các giao dịch bị "treo" (kẹt ở trạng thái Pending mãi mãi do một service bị sập không kịp gửi báo cáo). Phải có hệ thống scan định kỳ hoặc cảnh báo để giải quyết thủ công các ca khó đẻ này.
Chốt hạ
Chuyển từ Monolithic lên Microservices thì sướng cái tay lúc scale, nhưng lại khổ cái đầu lúc quản lý dữ liệu. Saga Pattern không phải là viên đạn bạc, nó làm code của anh em phức tạp lên gấp đôi, rườm rà gấp ba vì phải viết thêm một nùi các logic "bù trừ".
Tuy nhiên, đối với các hệ thống yêu cầu độ tin cậy cao, đây là con đường độc đạo. Hiểu và áp dụng được Saga (chọn đúng Choreography hay Orchestration) chính là cột mốc đánh dấu sự chuyển mình từ một gã thợ gõ code (Coder) sang một Kỹ sư phần mềm (Software Engineer) thực thụ.
All rights reserved