Mẫu giao dịch phân tán TCC (Try-Confirm-Cancel)
1. Tổng quan
A. Định nghĩa
TCC (Try-Confirm-Cancel) là mẫu giao dịch phân tán dựa trên bù (compensation) phân rã một giao dịch phân tán thành ba thao tác cho mỗi dịch vụ tham gia — Try (đặt chỗ tài nguyên), Confirm (xác nhận đặt chỗ) và Cancel (hủy đặt chỗ) — sao cho khi Try của mọi dịch vụ thành công thì gọi Confirm, còn nếu bất kỳ cái nào thất bại thì gọi Cancel, qua đó hiện thực hóa ngữ nghĩa commit hai pha ở mức ứng dụng.
TCC trở nên rõ ràng khi hiểu như một mẫu nâng cấu trúc "chuẩn bị (prepare)-commit" của 2PC lên thành logic nghiệp vụ. Trong khi 2PC dựa vào khóa và nhật ký undo/redo của trình quản lý tài nguyên (DB) để giữ tài nguyên, TCC khiến mỗi dịch vụ tự phơi bày ba API lũy đẳng (idempotent) để đặt chỗ, xác nhận và hủy tài nguyên của chính nó, và một bộ điều phối gọi ba thao tác này theo hai pha (pha 1 Try, pha 2 Confirm/Cancel). Vì vậy TCC thường được gọi là "2PC ở mức ứng dụng (application-level 2PC)" hay giao dịch dựa trên bù (compensation-based). Gốc rễ tư tưởng của nó nằm ở khái niệm thao tác tạm thời (tentative operation) · đặt chỗ (reservation) mà Pat Helland trình bày trong bài báo CIDR 2007 Life beyond Distributed Transactions, và nó đã trở thành mẫu thực tiễn được dùng rộng rãi qua các hiện thực như chế độ Seata TCC của Alibaba.
B. Bối cảnh ra đời và sự cần thiết
Kiến trúc microservices (MSA) tuân theo nguyên tắc DB per Service, nên một nghiệp vụ (ví dụ: đơn hàng thương mại điện tử = trừ tài khoản + giảm tồn kho + tích điểm) được thực thi trải trên các dịch vụ và DB vật lý khác nhau. Không còn có thể bảo đảm tính nguyên tử bằng một COMMIT duy nhất như thời monolith, nên cần một chiến lược giao dịch phân tán. Giải pháp truyền thống commit hai pha (2PC/XA) cho tính nhất quán mạnh, nhưng sau pha 1 prepare, nó giữ khóa DB trong thời gian dài cho đến khi commit hoàn tất, làm giảm thông lượng và tính sẵn sàng, và có vấn đề chặn (blocking) trong đó các bên tham gia đứng im khi đang giữ tài nguyên lúc bộ điều phối gặp sự cố. Điều này không phù hợp với lưu lượng Internet và tài chính quy mô lớn.
Saga đạt được tính sẵn sàng bằng cách xử lý từng bước như một giao dịch cục bộ được commit ngay lập tức và sửa chữa thất bại bằng bù hậu kỳ, nhưng có điểm yếu là thay đổi trung gian bị phơi bày ra ngoài ngay lập tức (dirty read) và không có tính cô lập. Ví dụ, nếu dịch vụ thanh toán thực sự rút tiền rồi bước tồn kho thất bại, tài khoản khách hàng đã hiển thị khoản rút cho đến khi hoàn tiền (bù) diễn ra.
TCC là sự dung hòa giữa hai thái cực này. Ý tưởng cốt lõi là "đừng commit ngay, hãy đặt chỗ trước." Ở pha 1 Try, thay vì tiêu thụ tài nguyên thật, nó chỉ cô lập và đóng băng một phần (ví dụ: tồn kho khả dụng 100 → khả dụng 90 · đóng băng 10), và chỉ sau khi đặt chỗ của mọi bên tham gia thành công thì pha 2 Confirm mới thực sự xác nhận phần đã đóng băng. Nhờ đó nó bảo đảm một tính bán-cô-lập không giữ khóa DB lâu (so với 2PC) trong khi ức chế việc phơi bày trạng thái trung gian nhờ đóng băng tài nguyên (so với Saga). Nói cách khác, TCC là chiến lược "cô lập về mặt logic bằng cách đặt trạng thái đặt chỗ lên dữ liệu nghiệp vụ, thay vì khóa tài nguyên về mặt vật lý."
2. Cấu trúc tổng thể và nguyên lý vận hành
Hệ thống TCC gồm bộ điều phối giao dịch (TM/TC) chỉ huy giao dịch toàn cục, và các bên tham gia TCC (Resource) mỗi bên hiện thực ba giao diện try/confirm/cancel. Bộ điều phối gọi Try của mọi bên tham gia ở pha 1 và, tùy kết quả, gọi Confirm cho tất cả hoặc Cancel cho tất cả ở pha 2.
flowchart TB
subgraph APP["Ứng dụng (bắt đầu giao dịch toàn cục)"]
TM["Bộ điều phối giao dịch (TM/TC)"]
end
subgraph P1["Dịch vụ tài khoản"]
A1["try: đóng băng số tiền rút"]
A2["confirm: đóng băng → rút thật"]
A3["cancel: giải phóng đóng băng"]
end
subgraph P2["Dịch vụ tồn kho"]
B1["try: đặt chỗ tồn kho (khả dụng↓ · đóng băng↑)"]
B2["confirm: đặt chỗ → trừ thật"]
B3["cancel: giải phóng đặt chỗ"]
end
subgraph P3["Dịch vụ điểm thưởng"]
C1["try: đặt chỗ tích điểm"]
C2["confirm: xác nhận tích điểm"]
C3["cancel: hủy đặt chỗ"]
end
TM -- "Pha 1 Try" --> A1 & B1 & C1
TM -- "Pha 2 Confirm (nếu tất cả thành công)" --> A2 & B2 & C2
TM -- "Pha 2 Cancel (nếu có cái thất bại)" --> A3 & B3 & C3
Cốt lõi của cấu trúc trên là mỗi tài nguyên tự chịu trách nhiệm cho ba chuyển tiếp trạng thái của chính nó. Bộ điều phối chỉ bảo đảm thứ tự gọi hai pha mà không biết ý nghĩa nghiệp vụ "đặt chỗ, xác nhận hay hủy cái gì," còn thao tác dữ liệu thực tế do mỗi dịch vụ đảm nhận. Nhờ sự phân tách mối quan tâm này, bộ điều phối có thể tiến hóa như một framework tổng quát và logic tài nguyên có thể tiến hóa độc lập theo từng miền.
Nhìn luồng bình thường và luồng thất bại theo trục thời gian như sau. Nếu chỉ một Try thất bại ở pha 1, bộ điều phối lập tức chuyển pha 2 sang Cancel, và chỉ tiến tới Confirm khi tất cả thành công.
sequenceDiagram
participant TM as Bộ điều phối (TC)
participant Acc as Tài khoản
participant Inv as Tồn kho
participant Pt as Điểm thưởng
Note over TM,Pt: Pha 1 — Try (đặt chỗ)
TM->>Acc: try (đóng băng rút 10.000 won)
Acc-->>TM: OK
TM->>Inv: try (đặt chỗ 1 sản phẩm)
Inv-->>TM: OK
TM->>Pt: try (đặt chỗ tích 100p)
Pt-->>TM: FAIL (vượt hạn mức)
Note over TM,Pt: Pha 2 — có cái thất bại → Cancel tất cả
TM->>Acc: cancel (giải phóng đóng băng)
TM->>Inv: cancel (giải phóng đặt chỗ)
TM->>Pt: cancel (rollback rỗng)
Note right of TM: nếu mọi Try đã thành công<br/>thì gọi confirm thay thế
A. Pha 1 — Try (đặt chỗ và kiểm chứng tài nguyên)
Try là bước thực hiện kiểm tra khả năng nghiệp vụ + cô lập tài nguyên. Nó không chỉ dừng ở xác nhận "có làm được không"; nó đóng băng (freeze) trước các tài nguyên cần thiết để Confirm tiếp theo chắc chắn thành công. Ví dụ, thay vì trừ ngay 10.000 won khỏi số dư, Try của dịch vụ tài khoản chỉ dịch chuyển trạng thái thành số dư khả dụng -= 10.000, số dư đóng băng += 10.000. Như vậy một giao dịch khác đến đồng thời không thể dùng trùng cùng 10.000 won đó, nên tính cô lập được bảo đảm, và vì việc rút thật chưa xảy ra nên việc hoàn tác (Cancel) rất rẻ. Do đó chất lượng thiết kế Try quyết định tính toàn vẹn của toàn bộ TCC, và nguyên tắc cốt lõi là "kiểm chứng và đặt chỗ đầy đủ trong Try đến mức Confirm không thể thất bại."
B. Pha 2 — Confirm (xác nhận đặt chỗ)
Confirm là bước thực sự tiêu thụ các tài nguyên đã đóng băng trong Try. Với dịch vụ tài khoản, nó chốt khoản rút bằng số dư đóng băng -= 10.000, còn với dịch vụ tồn kho, nó trừ phần đã đặt chỗ khỏi tồn kho thật. Tiền đề thiết kế quan trọng của Confirm là nó không bao giờ được thất bại. Vì mọi kiểm chứng và đặt chỗ đã hoàn tất trong Try, Confirm không thực hiện kiểm tra thêm mà chỉ xác nhận tài nguyên đã đặt chỗ. Nếu Confirm thất bại tạm thời do lỗi mạng, bộ điều phối thử lại cho đến khi thành công (at-least-once), nên Confirm phải lũy đẳng (idempotent). Tiền đề "Confirm được làm cho thành công bằng cách thử lại" này là nét riêng của TCC, khác với bù của Saga.
C. Pha 2 — Cancel (hủy đặt chỗ)
Cancel là bước khôi phục về trạng thái ban đầu các tài nguyên đóng băng của những bên tham gia đã thành công, khi chỉ một Try thất bại ở pha 1. Cancel của tài khoản hoàn tác bằng số dư đóng băng -= 10.000, số dư khả dụng += 10.000. Vì Try chỉ đóng băng tài nguyên chứ không thực sự tiêu thụ, Cancel đơn giản và an toàn hơn bù ngữ nghĩa của Saga (một giao dịch xuôi mới như hoàn tiền). Cancel cũng có thể bị thử lại nên phải lũy đẳng.
3. Ba thách thức toàn vẹn của TCC (theo Seata)
Có ba tình huống bất thường phải giải quyết khi thực sự hiện thực TCC, và tài liệu nguyên lý thiết kế TCC của Apache Seata tổ chức chúng một cách chuẩn mực. Không xử lý được ba thách thức này sẽ khiến tài nguyên đóng băng bị khóa vĩnh viễn hoặc dữ liệu bị hỏng.
| Thách thức | Tình huống | Cách xử lý chuẩn |
|---|---|---|
| Lũy đẳng (Idempotence) | Try/Confirm/Cancel bị gọi hai lần trở lên do thử lại mạng | Ghi trạng thái giao dịch vào bảng nhật ký và bỏ qua các yêu cầu đã xử lý (kết quả thực thi một lần và nhiều lần là như nhau) |
| Rollback rỗng (Empty Rollback) | Cancel được gọi trước dù Try chưa bao giờ đến | Ghi lại việc Try có chạy hay không; nếu không có bản ghi Try thì Cancel không làm gì và trả về thành công |
| Treo lơ lửng (Hanging) | Một Try bị trễ đến muộn hơn Cancel, để lại đóng băng vĩnh viễn | Cancel để lại nhật ký, và Try từ chối thực thi nếu bản ghi Cancel đó tồn tại |
Lũy đẳng là cần thiết vì giao tiếp trong môi trường phân tán vốn là at-least-once. Bộ điều phối gọi lại cho đến khi thành công khi không nhận được phản hồi, nhưng thực tế thường là lần gọi trước đã thành công mà chỉ phản hồi bị mất. Nếu Confirm không lũy đẳng trong trường hợp này, khoản rút bị chốt hai lần, gây trừ kép (double deduction). Trong thực tế điều này được hiện thực bằng cách giữ một trường trạng thái theo ID giao dịch và "không xác nhận lại nếu trạng thái đã là CONFIRMED," hoặc kiểm tra số dòng bị ảnh hưởng của một UPDATE và bỏ qua nếu bằng không.
Rollback rỗng xảy ra khi bộ điều phối không nhận được kết quả của lệnh gọi Try (Try không bao giờ đến bên tham gia do vấn đề mạng), quyết định rollback giao dịch và gọi Cancel. Đứng từ góc độ bên tham gia, đây là yêu cầu hủy một tài nguyên chưa từng đặt chỗ, nên nếu không có bản ghi Try thì Cancel phải là thao tác rỗng (no-op) và trả về thành công. Nếu không, việc khôi phục tài nguyên vô điều kiện sẽ giải phóng một đóng băng không tồn tại và tạo ra lỗi làm tăng số dư.
Treo lơ lửng (suspension) là rủi ro kế tiếp của rollback rỗng. Nếu một Try đến muộn sau khi xử lý Cancel do trễ mạng, Try này đóng băng tài nguyên, nhưng vì giao dịch toàn cục đã kết thúc nên Confirm/Cancel vốn sẽ giải phóng đóng băng đó không bao giờ đến nữa. Kết quả là tài nguyên đóng băng bị khóa vĩnh viễn. Giải pháp là để Cancel ghi vào nhật ký rằng "giao dịch này đã bị hủy," và để Try đến muộn kiểm tra bản ghi đó rồi tự từ chối thực thi. Vì ba thách thức này đan xen nhau, chúng thường được quản lý chung bằng một bảng nhật ký trạng thái giao dịch duy nhất.
4. So sánh với 2PC và Saga
Cả ba mẫu đều xử lý giao dịch phân tán, nhưng chúng khác nhau cơ bản ở tính nhất quán, tính cô lập và cách giữ tài nguyên. Nhìn vào "tại sao khác biệt đó phát sinh" thay vì một danh sách liệt kê đơn giản sẽ làm rõ tiêu chí lựa chọn.
| Phân loại | 2PC (XA) | TCC | Saga |
|---|---|---|---|
| Tính nhất quán | Nhất quán mạnh | Bán-mạnh (thực chất là kết quả cuối với cửa sổ ngắn) | Nhất quán cuối |
| Tính cô lập | Bảo đảm bằng khóa DB | Bán-cô-lập nhờ đóng băng tài nguyên | Không (phơi bày trung gian) |
| Cách rollback | Nhật ký undo của DB | Cancel (giải phóng đặt chỗ) | Giao dịch bù ngữ nghĩa |
| Giữ tài nguyên | Dài (khóa đến khi commit) | Ngắn (chỉ đóng băng) | Hầu như không |
| Tính xâm lấn | Thấp (DB xử lý) | Cao (hiện thực 3 API) | Trung bình (logic bù) |
| Lĩnh vực phù hợp | Ít DB, đồng nhất | Tài chính, thanh toán, yêu cầu nhất quán mạnh | Luồng chạy dài, ghép lỏng |
Ở 2PC, trình quản lý tài nguyên DB xử lý cô lập và khôi phục thay cho ứng dụng bằng khóa và nhật ký, nên tính xâm lấn ứng dụng thấp, nhưng trả giá bằng việc giữ khóa lâu, mở rộng kém. TCC chuyển trách nhiệm cô lập và khôi phục sang ứng dụng và giảm việc giữ khóa xuống mức đóng băng, nhưng vì mọi dịch vụ tham gia phải trực tiếp hiện thực ba API nên chi phí phát triển và tính xâm lấn cao. Saga từ bỏ hoàn toàn tính cô lập, bù lại hiện thực lỏng lẻo nhất và phù hợp với luồng chạy dài, nhưng trạng thái trung gian bị phơi bày, khiến nó không phù hợp cho giao dịch tài chính nơi tính cô lập quan trọng. Do đó lĩnh vực áp dụng điển hình của TCC là các khu vực như thanh toán, chuyển khoản và trừ tồn kho đòi hỏi "nhất quán mạnh và giữ tài nguyên ngắn cùng lúc," trong khi Saga phù hợp hơn với các luồng công việc lỏng lẻo (toàn bộ luồng đặt chỗ du lịch, v.v.).
5. Chuyên sâu — Áp dụng thực tiễn và xu hướng framework
TCC đặc biệt được dùng rộng rãi trong các giao dịch tài chính lưu lượng cao của các công ty Internet lớn ở Trung Quốc. Tiêu biểu, các hệ thống thanh toán và tài khoản thuộc nhóm Alibaba/Ant xử lý hàng chục nghìn lượt chuyển khoản mỗi giây và không thể chịu được việc giữ khóa của 2PC, nên đã chọn TCC dựa trên đóng băng tài nguyên làm mẫu cốt lõi. Phiên bản mã nguồn mở của kinh nghiệm này là chế độ TCC của Apache Seata: lập trình viên khai báo phương thức Try bằng @TwoPhaseBusinessAction và chỉ định tên phương thức commit/rollback, sau đó bộ điều phối giao dịch (TC) của Seata quản lý nhật ký giao dịch toàn cục và hỗ trợ gọi hai pha, thử lại, cùng các phòng vệ lũy đẳng/rollback-rỗng/treo-lơ-lửng. Ngoài ra, các framework TCC trong hệ sinh thái Java như Hmily, ByteTCC và TCC-Transaction cung cấp các mô hình tương tự.
Lấy ví dụ cụ thể là chuyển khoản ngân hàng: Try của tài khoản rút đóng băng số tiền chuyển, còn Try của tài khoản nhận tạo một bản ghi "chờ ghi có." Khi cả hai Try thành công, trong Confirm tài khoản rút thực sự trừ phần đã đóng băng và tài khoản nhận phản ánh số tiền chờ vào số dư thật. Nếu một trong hai thất bại, Cancel hoàn tác các bản ghi đóng băng/chờ. Trong quá trình này, số dư khả dụng mà khách hàng thấy giảm ngay tại Try, nhưng vì khoản rút kế toán thực tế được chốt tại Confirm nên việc dùng trùng bị chặn về cơ bản. Trong một ví dụ khác, đặt vé máy bay, Try ghế đặt chỗ một ghế cụ thể ở trạng thái "HOLD" trong vài phút, và sau khi Try thanh toán thành công, Confirm chốt nó thành "CONFIRMED" — trải nghiệm "thanh toán trong N phút" giữ ghế quen thuộc chính là gắn với mô hình đóng băng tài nguyên của TCC.
Về xu hướng gần đây, để giảm chi phí hiện thực cao của TCC thuần túy (mọi dịch vụ viết gấp ba lần số API), Seata cung cấp, bên cạnh TCC, một chế độ AT (Automatic Transaction) (một proxy DB tự động sinh nhật ký undo để tối thiểu hóa công việc của lập trình viên), và các thiết kế trộn các chế độ TCC/AT/Saga/XA tùy tình huống đang lan rộng. Hơn nữa, các thiết kế giao dịch lai (hybrid) kết hợp sự kiện đáng tin cậy dựa trên thông điệp (Transactional Outbox) với TCC — xử lý bằng TCC chỉ những bước cốt lõi thực sự cần nhất quán mạnh và phần còn lại lỏng lẻo bằng sự kiện — đang trở thành dòng chính trong thực tiễn.
6. Cân nhắc và hàm ý (góc nhìn Kỹ sư chuyên nghiệp)
Việc áp dụng TCC không chỉ là lựa chọn thư viện mà kéo theo đánh đổi trên toàn bộ mô hình dữ liệu, vận hành và tổ chức, nên cần phán đoán chiến lược những điều sau.
Chọn lọc phạm vi áp dụng (chiến lược áp dụng): Bọc mọi giao dịch bằng TCC làm bùng nổ chi phí phát triển và vận hành. Thực tế là nên áp dụng có chọn lọc chỉ cho các nghiệp vụ cốt lõi đồng thời cần cô lập mạnh và giữ ngắn (thanh toán, chuyển khoản, tồn kho), và tách các luồng lỏng lẻo sang Saga/sự kiện theo một chiến lược trộn. Điểm then chốt là năng lực thiết kế "áp dụng mẫu một cách phân biệt theo mức độ nhất quán được yêu cầu."
Tính xâm lấn hiện thực và độ khó thiết kế (đánh đổi): TCC yêu cầu mỗi dịch vụ tham gia hiện thực ba thao tác — Try, Confirm và Cancel — cộng với phòng vệ lũy đẳng, rollback-rỗng và treo-lơ-lửng, nên khối lượng mã và gánh nặng kiểm thử cho mỗi dịch vụ tăng đáng kể. Đặc biệt, để làm cho Confirm "không bao giờ thất bại," việc đặt chỗ đầy đủ trong Try là tiền đề, nên một thiết kế xâm lấn thêm các trường trạng thái đóng băng/đặt chỗ vào mô hình dữ liệu miền là không thể tránh. Việc áp dụng bị hạn chế khi không thể ép buộc ba API, như với dịch vụ cũ hoặc dịch vụ bên ngoài.
Tác dụng phụ của cô lập và khả năng quan sát (vận hành): Bán-cô-lập của TCC giả định một cửa sổ "đóng băng tài nguyên" ngắn, nên nếu thời gian từ Try đến Confirm kéo dài, tài nguyên đóng băng chất chồng và có thể gây cạn kiệt dung lượng hoặc khóa chết (deadlock). Vì các giao dịch chưa hoàn tất còn sót do sự cố bộ điều phối hoặc bên tham gia không phản hồi để lại đóng băng không được giải quyết, khả năng quan sát vận hành là bắt buộc: giám sát nhật ký giao dịch toàn cục, Cancel tự động dựa trên timeout, và một console can thiệp thủ công cho các giao dịch chưa xử lý.
Kiểm chứng toàn vẹn dữ liệu và khắc phục (triển vọng, công nghệ liên quan): Vì phải giữ tính đúng đắn bằng khóa lũy đẳng và nhật ký trạng thái trên giao tiếp at-least-once, cần đặt một lưới an toàn gồm các batch đối soát (reconciliation) định kỳ kiểm tra chéo trạng thái đóng băng-xác nhận. Các công nghệ liên quan như Saga, Transactional Outbox và CDC kết hợp một cách tự nhiên, và giữa sự tiến hóa đa chế độ của các framework lưới giao dịch (Seata, v.v.) và sự lan rộng của kiến trúc serverless/sự kiện, TCC được kỳ vọng định vị như một "công cụ chính xác cho những điểm cần nhất quán mạnh."
Tài liệu tham khảo
- Apache Seata, "TCC Mode Design Principle", https://seata.apache.org/blog/tcc-mode-design-principle
- Apache Seata, "TCC Mode" User Docs, https://seata.apache.org/docs/user/mode/tcc/
- Pat Helland, "Life beyond Distributed Transactions: an Apostate's Opinion", CIDR 2007, https://ics.uci.edu/~cs223/papers/cidr07p15.pdf
Tóm tắt một câu: TCC phân rã một giao dịch phân tán thành các thao tác Try (đặt chỗ), Confirm (xác nhận) và Cancel (giải phóng) theo từng dịch vụ để đạt được, mà không cần việc giữ khóa dài của 2PC, một tính bán-cô-lập mạnh hơn Saga — một mẫu commit hai pha ở mức ứng dụng mà thành bại phụ thuộc vào các phòng vệ lũy đẳng, rollback-rỗng và treo-lơ-lửng.