Nguyen Le PhongNguyen Le Phong

Saga Pattern cho distributed transaction

Một bài giải thích thực tế về Saga pattern trong distributed transaction: vì sao một database transaction không còn đủ khi hệ thống tách service, orchestration và choreography khác nhau thế nào, và vì sao compensation, observability, idempotency lại quan trọng.

Một đồng đội mở dashboard đơn hàng sau giờ ăn trưa và chỉ vào một dòng với nụ cười hơi mệt. Payment đã thành công, inventory reservation thất bại, email vẫn được gửi, và support đang hỏi rốt cuộc khách hàng có sở hữu sản phẩm hay không. Không ai làm ẩu. Chỉ là luồng công việc đã đi qua quá nhiều service boundary để một transaction đơn giản có thể bảo vệ tất cả.

Trong một database duy nhất, transaction tạo cảm giác rất yên tâm. Hoặc tất cả write cùng commit, hoặc tất cả rollback. Một dòng order, một dòng payment, một update stock có thể đi cùng nhau trong một boundary rõ. Nhưng trong distributed system, boundary đó thường biến mất. Payment có thể nằm ở một service, inventory ở service khác, shipping ở nơi khác, notification lại ở một chỗ khác. Mỗi service sở hữu data riêng, và cố dùng một database transaction cho tất cả thường kéo theo coupling chặt, lock lâu, hoặc gánh nặng vận hành mà team không thật sự muốn.

Saga pattern là một cách đối diện với thực tế đó. Thay vì giả vờ toàn bộ business process có thể là một atomic transaction, saga chia nó thành chuỗi local transaction. Mỗi bước commit trong service của mình. Nếu một bước sau thất bại, hệ thống chạy compensating action để đảo ngược hoặc giảm tác động của các bước trước. Nó không giống một nút save khổng lồ, mà giống một workflow cẩn thận, nơi từng người tham gia biết bước kế tiếp và biết phải làm gì khi kế hoạch đổi hướng.

Ví dụ quen thuộc là checkout. Order service tạo pending order. Payment service charge khách hàng. Inventory reserve stock. Shipping chuẩn bị delivery request. Nếu inventory không reserve được stock sau khi payment thành công, inventory service không thể đơn giản rollback database của payment. Hệ thống cần một compensating action, như refund hoặc void charge, rồi đánh dấu order thất bại. Customer experience vẫn cần thông báo rõ ràng, và support cần đủ evidence để hiểu chuyện gì đã xảy ra.

Có hai cách phổ biến để điều phối saga. Với orchestration, một coordinator trung tâm ra lệnh cho từng service: tạo order, charge payment, reserve stock, arrange shipping, gửi email. Coordinator cũng quyết định compensation nào sẽ chạy khi một bước fail. Cách này dễ hiểu hơn vì workflow nằm ở một nơi. Trade-off là coordinator có thể biết quá nhiều về từng service và trở nên quá quan trọng trong mọi release.

Với choreography, không có conductor trung tâm. Các service publish event và phản ứng với event của nhau. Order service publish OrderCreated. Payment lắng nghe và publish PaymentCaptured. Inventory lắng nghe và publish StockReserved hoặc StockReservationFailed. Cách này giảm direct coupling, nhưng cũng có thể làm toàn bộ business flow khó nhìn hơn. Một engineer mới có thể phải lần qua nhiều event handler ở nhiều repository chỉ để hiểu vì sao một email cho khách hàng được gửi.

Không có kiểu nào luôn tốt hơn. Orchestration thường hữu ích khi process rất quan trọng, có nhiều nhánh, và cần một owner rõ. Choreography có thể hợp khi các bước đơn giản hơn, service đã giao tiếp bằng event, và team có observability tốt quanh event flow. Câu hỏi không phải pattern nào nghe thanh lịch hơn. Câu hỏi là pattern nào team có thể debug lúc 4 giờ chiều một ngày bình thường, khi một bước fail âm thầm và khách hàng đang đợi.

Compensation là phần cần được thiết kế thật trung thực. Không phải action nào cũng undo sạch được. Bạn có thể refund payment, nhưng không thể unsend một email. Bạn có thể release inventory, nhưng không thể xóa hết sự bối rối nếu confirmation message đã tới quá sớm. Vì vậy thiết kế saga vừa là technical thinking, vừa là product thinking. Team cần quyết định state nào được hiển thị, message nào nên trì hoãn tới khi process ổn định, và failure nào cần human review.

Idempotency là một yêu cầu âm thầm khác. Distributed system sẽ retry. Message có thể tới hai lần. Timeout có thể che đi một operation thật ra đã thành công. Nếu bước payment chạy hai lần vì caller không nhận được response đầu tiên, hệ thống không được charge khách hai lần. Mỗi bước cần operation key ổn định, cách nhận ra duplicate request, và response dự đoán được khi cùng command tới lại. Thiếu idempotency, một saga có thể biến một delay có thể phục hồi thành incident thứ hai.

Observability là thứ làm saga sống được trong production. Mỗi bước nên mang correlation ID. Log nên có saga id, current state, command, event, retry count, compensation action và failure reason. Metric nên cho thấy saga bị kẹt, tỷ lệ compensation, thời gian hoàn thành trung bình, và bước nào fail nhiều nhất. Một admin view nhỏ đôi khi quý hơn thêm một diagram, vì nó giúp operator trả lời câu hỏi đơn giản: business process này đang ở đâu?

Saga pattern không làm distributed transaction trở nên đơn giản. Nó làm sự phức tạp đủ rõ để quản lý. Team đổi một database boundary mạnh lấy một process nhìn thấy được, local commit, compensating action và failure handling cẩn thận. Trade-off này hợp lý khi service ownership và scaling thật sự quan trọng. Nó là ceremony không cần thiết khi một database và một transaction đã đủ phục vụ sản phẩm.

Lần tới khi một business flow đi qua nhiều service, hãy thử sketch những phần boring trước. Cái gì commit local? Cái gì có thể fail? Cái gì có thể compensate? Cái gì tuyệt đối không được chạy hai lần? Support sẽ thấy gì khi process bị kẹt? Nếu bạn từng sống qua một distributed workflow dở dang, ký ức đó thường là người thầy tốt nhất. Nó nhắc rằng architecture không chỉ là nối các service với nhau; nó còn là tạo đường quay về rõ ràng khi kết nối bị gãy.

Bạn thấy bài viết thế nào?