Hãy tưởng tượng bạn đang lên kế hoạch cho một chuyến du lịch Đà Nẵng hoàn hảo cùng gia đình. Bạn vào một nền tảng du lịch (OTA), chọn ngay một combo cực kỳ tiện lợi và bấm "Thanh toán". Lệnh đặt hàng của bạn bao gồm 3 dịch vụ: vé máy bay Vietnam Airlines, phòng khách sạn qua Agoda, và một chiếc xe tự lái trên Mioto.

Phía sau giao diện bóng bẩy đó, hệ thống bắt đầu xử lý hàng loạt giao dịch ngầm:

  • Đặt vé máy bay: Thành công, đã giữ chỗ.
  • Đặt phòng khách sạn: Thành công, đã khóa phòng.
  • Thuê xe tự lái: Thất bại, vì đúng ngày đó ở Đà Nẵng đã hết sạch xe.

Lúc này, một thảm họa logic có thể xảy ra: Vé máy bay đã xuất, tiền phòng đã trừ, nhưng tour của bạn lại không hoàn chỉnh. Nếu hệ thống không tự động biết cách "hủy bỏ" 2 dịch vụ kia, điều gì sẽ chờ đợi bạn?

  • Bạn mất tiền oan cho một chuyến đi không trọn vẹn.
  • Nhân viên chăm sóc khách hàng phải nhảy vào xử lý thủ công (gọi điện xin lỗi, làm lệnh hoàn tiền).
  • Chỗ ngồi máy bay và phòng khách sạn bị "khóa" lãng phí trong khi có người khác đang muốn mua.

Câu chuyện này không phải là một khái niệm trừu tượng, nó là bài toán nghiệp vụ (business) sát sườn mà bất kỳ hệ thống nào cũng phải đối mặt.

Tại sao không dùng cách "truyền thống"?

Nếu bạn từng học về Cơ sở dữ liệu, bạn sẽ biết đến chuẩn ACID và cách cũ để giải quyết bài toán này là 2-Phase Commit (2PC).

Để dễ hình dung, 2PC giống như một "trọng tài" đứng giữa và hỏi cả 3 dịch vụ (Hàng không, Khách sạn, Thuê xe): "Mọi người đã khóa tài nguyên xong chưa, sẵn sàng chốt (commit) chưa?".

  • Nếu tất cả hô "Sẵn sàng", trọng tài hô "Chốt!".
  • Nếu chỉ một người lắc đầu, trọng tài hô "Hủy bỏ toàn bộ (Rollback)!".

Nghe rất hoàn hảo, nhưng tại sao chúng ta không thể dùng nó trong kiến trúc Microservices hay Serverless hiện đại?

  • Không cùng một nhà: 3 dịch vụ thuộc về 3 công ty khác nhau. Bạn không thể bắt hệ thống của Vietnam Airlines, Agoda và Mioto dùng chung một giao thức transaction.
  • Bản chất Stateless: Các kiến trúc Serverless (như AWS Lambda) hoạt động theo kiểu "gọi xong là quên" (stateless), không duy trì kết nối liên tục để giữ bối cảnh giao dịch.
  • Tắc nghẽn hệ thống: Trong lúc chờ tất cả cùng "Sẵn sàng", tài nguyên bị khóa chặt. Nếu mạng bị lag, toàn bộ hệ thống sẽ đứng hình.

Câu chốt: 2PC được sinh ra với giả định rằng mọi hệ thống luôn online và đồng bộ ngay lập tức. Trong thực tế, Internet không hoạt động như vậy.

Saga Pattern: Cách tiếp cận "Undo" của đời thực

Thay vì cố gắng khóa mọi thứ lại, các kỹ sư phần mềm chọn cách chấp nhận thực tế.

Quay lại ví dụ đặt tour Đà Nẵng ở trên. Nếu bước thuê xe thất bại, bạn không thể dùng phép thuật để "quay ngược thời gian" (rollback) hủy lệnh đặt vé và phòng ngay lập tức như chưa từng có chuyện gì xảy ra. Dữ liệu đã được ghi nhận ở hệ thống của Vietnam Airlines và Agoda rồi.

Thay vào đó, bạn phải làm các bước bù trừ:

  1. Gửi lệnh Hủy phòng khách sạn (và nhận lại tiền phòng).
  2. Gửi lệnh Hủy vé máy bay (hoàn vé).

Đây chính xác là cách Saga Pattern hoạt động.

Về mặt kỹ thuật, Saga là một chuỗi các giao dịch cục bộ (local transaction). Khi một bước bị lỗi, hệ thống sẽ kích hoạt các Compensating Transaction (Giao dịch bù trừ) để "undo" về mặt nghiệp vụ các bước đã thực hiện thành công trước đó.

  • 2PC: Rollback về mặt kỹ thuật (database xóa sạch dấu vết).
  • Saga: Rollback về mặt nghiệp vụ (vết trong database vẫn còn, nhưng có một hành động mới sinh ra để bù trừ. Ví dụ: Không có "un-trừ tiền", chỉ có "hoàn tiền").

Hai trường phái triển khai Saga (và cách ứng dụng)

Để triển khai Saga trên stack AWS, chúng ta có 2 kiến trúc chính.

1. Choreography (Vũ điệu)

Hãy tưởng tượng một nhóm nhảy đường phố không có biên đạo múa. Mỗi vũ công tự nhìn người kế bên để biết khi nào đến lượt mình.

Cách hoạt động: Các service giao tiếp với nhau qua một Event Bus (Ví dụ: RabbitMQ, Kafka, hoặc AWS EventBridge).

  • Service Hàng Không chạy xong -> Phát sự kiện: "Vé đã đặt!".
  • Service Khách Sạn nghe thấy -> Chạy tác vụ đặt phòng -> Phát sự kiện: "Phòng đã đặt!".
  • Service Thuê Xe nghe thấy -> Đặt xe thất bại -> Phát sự kiện: "Lỗi đặt xe!".
  • Service Khách Sạn & Hàng Không nghe thấy lỗi -> Tự động chạy hàm hủy dịch vụ của mình.

Điểm cộng: Tính độc lập cực cao (Loose coupling). Thêm service mới rất dễ vì không ai phụ thuộc chặt chẽ vào ai.

Điểm trừ: Khó theo dõi luồng tổng thể. Khi có lỗi, bạn rất khó biết "vũ điệu" đang kẹt ở đâu.

2. Orchestration (Nhạc trưởng)

Giống như một dàn nhạc giao hưởng, có một vị nhạc trưởng (Conductor / Workflow Engine) đứng giữa cầm đũa chỉ huy từng nhạc cụ chơi hay dừng.

Cách hoạt động: Có một bộ điều phối trung tâm (Ví dụ: Camunda, Temporal, hoặc AWS Step Functions).

  • Nhạc trưởng ra lệnh: "Hàng Không, đặt vé!" -> OK.
  • Nhạc trưởng ra lệnh: "Khách Sạn, đặt phòng!" -> OK.
  • Nhạc trưởng ra lệnh: "Thuê Xe, đặt xe!" -> LỖI.
  • Nhạc trưởng lập tức tra cứu kịch bản và ra lệnh ngược lại: "Khách Sạn, hủy phòng! Hàng Không, hủy vé!".

Điểm cộng: Quản lý quy trình tập trung. Bắt lỗi, thử lại (retry) và theo dõi tiến độ (tracking) cực kỳ trực quan.

Điểm trừ: Bộ điều phối trở thành "điểm thắt nút" trung tâm, tạo ra sự liên kết chặt chẽ hơn giữa các dịch vụ.

So sánh thực chiến: Khi nào dùng cái nào?

Tiêu chíChoreography (EventBridge)Orchestration (Step Functions)
Độ phức tạp nghiệp vụPhù hợp logic đơn giản, luồng thẳng.Phù hợp logic phức tạp, nhiều nhánh rẽ, điều kiện.
Tính độc lậpNhiều domain độc lập không muốn đụng chạm code nhau.Step functions tạo ra "Coupling".
Yêu cầu Audit / TrackingKhó. Dữ liệu phân tán ở nhiều log khác nhau.Tuyệt vời. Mọi bước được visualize trên AWS Console, lịch sử lưu rõ ràng.
Gỡ lỗi (Debug)Khó nhận biết Saga đang kẹt ở bước nào.Trực quan, bấm vào State Machine là thấy.

Ba bẫy chí mạng khi làm Saga và Cách hóa giải

Bẫy 1: Quên mất tính "Idempotency" (Bất biến)

  • Vấn đề: Mạng chập chờn, Step Functions Retry lại BookFlight Lambda. Nếu không cẩn thận, khách hàng bị trừ tiền 2 lần.
  • Giải pháp: Bắt buộc mọi yêu cầu (request) phải đính kèm một mã giao dịch duy nhất (idempotency_key). Khi ghi vào Database, phải kiểm tra xem mã này đã tồn tại chưa (Sử dụng Unique Constraint trong SQL hoặc Conditional Write trong NoSQL). Nếu đã có, hệ thống chỉ trả về kết quả cũ chứ không tạo mới.

Code minh họa thực tế:

import boto3
from botocore.exceptions import ClientError

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Bookings')

def process_booking(event, context):
    idempotency_key = event['idempotency_key'] # Dùng mã duy nhất từ request
    
    try:
        # Ghi vào DB với điều kiện: Mã này CHƯA TỪNG tồn tại
        table.put_item(
            Item={
                'idempotency_key': idempotency_key,
                'status': 'CONFIRMED',
                'flight_data': event['flight_data']
            },
            ConditionExpression='attribute_not_exists(idempotency_key)'
        )
        return {"status": "SUCCESS", "message": "Booked"}
        
    except ClientError as e:
        if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
            # Văng lỗi này nghĩa là Giao dịch đã được xử lý trước đó.
            # Ta chỉ cần trả về kết quả cũ, KHÔNG tạo mới.
            existing_item = table.get_item(Key={'idempotency_key': idempotency_key})
            return {"status": "SUCCESS", "message": "Already Booked"}
        raise e

Bẫy 2: Hàm bù trừ (Compensation) cũng thất bại

Vấn đề: Đặt xe thất bại, Step Functions gọi CancelHotel Lambda. Nhưng xui xẻo API của Agoda lúc đó sập. Nếu "nuốt lỗi", khách mất tiền phòng.

Giải pháp: Compensation không bao giờ được thiết kế để thất bại trong im lặng.

  • Exponential Backoff: Cấu hình Step Functions thử lại (Retry) CancelHotel với khoảng cách thời gian tăng dần (ví dụ: sau 2s, 4s, 8s, 16s) và MaxAttempts là 5 lần.
  • Alerting: Nếu thử hết 5 lần vẫn fail, luồng phải nhảy vào một Dead Letter Queue (SQS/DLQ). Từ đây, kích hoạt chuông báo động (Amazon SNS gửi vào Slack/PagerDuty) để xử lý.

Bẫy 3: Mù mờ về trạng thái (Thiếu Observability)

  • Vấn đề: Trong Choreography, nếu sếp hỏi "Khách hàng A đang kẹt ở bước nào rồi?", bạn sẽ phải mò mẫm grep log của 3 service khác nhau.
  • Giải pháp: Bắt buộc phải tích hợp Distributed Tracing (Truy vết phân tán) như AWS X-Ray hoặc OpenTelemetry. Bằng cách đính kèm một Trace ID duy nhất vào Event ngay từ API Gateway, bạn có thể nhìn thấy một biểu đồ đường đi (Service Map) chỉ rõ request đang nằm ở Lambda nào, tốn bao nhiêu mili-giây, và tắc nghẽn ở đâu.

Saga không phải lúc nào cũng là lựa chọn đúng

Trước khi quyết định dùng Saga, hãy trả lời 3 câu hỏi sau.

1. Nghiệp vụ có thực sự chịu được "dữ liệu không nhất quán tạm thời" không?

Saga hoạt động theo mô hình Eventual Consistency — có một khoảng thời gian (dù ngắn) mà dữ liệu ở các service chưa đồng bộ với nhau. Trong ví dụ đặt tour, sau khi BookFlight thành công nhưng RentCar chưa chạy xong, hệ thống đang ở trạng thái "nửa vời". Nếu lúc này một nhân viên vào tra cứu đơn hàng, họ sẽ thấy vé máy bay đã đặt nhưng xe chưa có.

Với hệ thống đặt tour, điều này chấp nhận được vì quy trình diễn ra trong vài phút. Nhưng với hệ thống kế toán, ngân hàng, hay bất kỳ nghiệp vụ nào mà tính nhất quán tức thời là bắt buộc theo quy định pháp lý — Saga là lựa chọn sai.

2. Team có khả năng "Dọn rác" (DLQ) không?

Saga đẩy cái khó từ Database sang vai đội ngũ Vận hành. Khi luồng hoàn tiền thất bại nhiều lần và rơi vào hàng đợi lỗi (DLQ), ai sẽ xử lý? Nếu team của bạn không có quy trình trực ban (On-call) hay kịch bản xử lý sự cố (Runbook), Saga sẽ biến thành một quả bom "nợ kỹ thuật" nguy hiểm.

3. Số lượng bước trong Saga có thực sự nhiều không?

Nếu một transaction chỉ span qua 2 service nội bộ, cùng chung database (PostgreSQL chẳng hạn), hoàn toàn có thể dùng distributed transaction (2PC) bình thường hoặc thậm chí một database transaction duy nhất. Saga chỉ thực sự phát huy giá trị khi:

  • Các service thuộc nhiều team/domain khác nhau.
  • Có ít nhất một bên thứ ba (third-party API) tham gia.
  • Quy trình có nhiều hơn 3 bước với logic rẽ nhánh phức tạp.

Tạm kết

Saga Pattern không phải là viên đạn bạc hoàn hảo để thay thế hoàn toàn giao dịch ACID truyền thống. Saga là sự đánh đổi: Bạn chấp nhận hy sinh việc dữ liệu đồng bộ ngay lập tức ở mọi nơi (Immediate Consistency), để đổi lấy khả năng hệ thống hoạt động mượt mà, không bị treo cứng và có thể mở rộng vô hạn (Scalability).

Tư duy cốt lõi của Saga rất giống với cuộc sống: Chúng ta không thể bắt mọi thứ xung quanh phải đứng im chờ đợi mình. Khi có lỗi xảy ra? Đừng cố gắng xóa sạch dấu vết như nó chưa từng tồn tại, hãy đối mặt và xử lý hậu quả bằng những hành động bù trừ thỏa đáng.