Từ ACID single-node tới giao dịch phân tán
Ôn ACID và isolation ở một node rồi hỏi: điều gì vỡ khi giao dịch trải trên nhiều node? Cầu nối từ khoá SQL sang bài toán phân tán.
TL;DR: Transaction cho hai đảm bảo chính: atomicity (mọi thao tác cùng commit hoặc cùng huỷ, không nửa vời) và isolation (giao dịch đồng thời không giẫm chân nhau, như chạy tuần tự). Trên một node, một write-ahead log và một lock/MVCC manager lo cả hai. Khi giao dịch trải trên nhiều node, cả hai vỡ: atomicity cần mọi node đồng ý commit dù mạng có thể chết giữa chừng — sinh ra 2PC; isolation cần ngăn anomaly xuyên node mà không có lock manager trung tâm. Bài này ôn ACID và chỉ rõ điều gì vỡ khi lên nhiều node, đặt nền cho các bài sau.
Bạn đã thấy transaction "chạy đẹp" trên một database. Câu hỏi của module: khi tài khoản A ở node 1 và tài khoản B ở node 2, làm sao "trừ A, cộng B" vẫn hoặc-cả-hai-hoặc-không-cái-nào — khi node 2 có thể chết đúng lúc node 1 vừa trừ xong? Trước khi giải, phải hiểu rõ cái gì trở nên khó.
Ở khoá SQL, bạn học ACID: Atomicity (all-or-nothing), Consistency (giữ ràng buộc), Isolation (giao dịch đồng thời không phá nhau), Durability (đã commit thì bền). Trên một node, atomicity và durability dựa vào write-ahead log; isolation dựa vào lock/MVCC. Bài này hỏi: hai trụ đó đứng thế nào khi dữ liệu trải nhiều node.
1. Analogy — hai két sắt ở hai thành phố
Trên một node, transaction như một két sắt có người gác: mọi thay đổi ghi vào sổ nháp, tới lúc "commit" thì đóng dấu một phát — hoặc xé bỏ toàn bộ nếu huỷ. Một người gác, một quyết định, không ai chen.
Giờ tiền của bạn nằm trong hai két ở hai thành phố (hai node), và bạn muốn chuyển từ két A sang két B. Không còn một người gác chung. Bạn nhắn hai nơi "trừ 100 ở A, cộng 100 ở B". Nếu A trừ xong mà B mất điện trước khi cộng — tiền bốc hơi. Nếu bạn bảo cả hai "chuẩn bị" rồi mới "chốt", nhưng người báo chốt lăn ra ngất giữa chừng — A không biết nên chốt hay huỷ, ngồi treo giữ tiền. Đây chính là hai cái khó: cùng quyết (atomicity) và không biết đối phương chết hay chậm.
| Két sắt | Transaction |
|---|---|
| Một két, một người gác | Single-node: một WAL, một lock manager |
| Đóng dấu một phát / xé bỏ | Commit / abort atomic |
| Hai két hai thành phố | Dữ liệu trên nhiều node |
| Trừ A xong, B mất điện | Atomicity vỡ (nửa vời) |
| Người báo chốt ngất giữa chừng | Coordinator chết → blocking |
Một node = một người gác, dễ. Nhiều node = nhiều két không người gác chung — phải bịa ra "nghi thức cùng chốt", và nghi thức đó chính là 2PC (và điểm yếu của nó).
2. Ôn nhanh — vì sao một node đủ để đảm bảo cả hai
Trên một node, cả atomicity (all-or-nothing) lẫn isolation (đồng thời như tuần tự) đứng được nhờ một điểm điều phối trung tâm: một write-ahead log lo atomicity (ghi ý định trước; commit = đóng một dấu "committed"; crash trước dấu đó thì lúc khởi động lại rollback phần dở), và một lock/MVCC manager duy nhất lo isolation (điều phối khoá hoặc ảnh chụp nhất quán cho mọi giao dịch).
Single-node transaction (chuyen tien trong 1 DB):
BEGIN
UPDATE acc SET bal = bal - 100 WHERE id = A -- ghi WAL
UPDATE acc SET bal = bal + 100 WHERE id = B -- ghi WAL
COMMIT -- 1 dau "committed" -> atomic
=> WAL + lock manager cua CUNG mot node lo atomicity + isolation.
Mấu chốt mang sang phần sau: cả hai đảm bảo cùng dựa vào MỘT thứ đó. Distributed transaction khó chính vì thứ đó không còn tồn tại một cách tự nhiên khi dữ liệu trải nhiều node — và phần dưới sẽ lần theo đúng cơ chế WAL để thấy nó vỡ ở đâu.
3. Điều gì vỡ khi lên nhiều node?
Đặt hai tài khoản ở hai node và thử "gọi hai update trong một khối code" như thể vẫn là một giao dịch. Trước khi xem nó vỡ ở đâu, hãy tự lần theo cơ chế WAL vừa ôn.
Node 1 vừa commit xong A -= 100 (dấu "committed" đã ghi bền vào WAL của riêng node 1). Đúng lúc đó node 2 chết trước khi kịp commit B += 100. Trong hệ có "thứ" nào tự động cuộn lại phần node 1 đã làm để giữ all-or-nothing không? Lần theo cơ chế WAL từng-node ở mục 2 rồi đoán — trước khi đọc tiếp.
Chuyen tien A (node 1) -> B (node 2), goi hai update trong 1 khoi code:
t0 Node1: BEGIN; A -= 100; ghi WAL rieng
t1 Node1: COMMIT -> WAL node1 dong dau "committed"
-> A giam 100 that su, da BEN (durable)
t2 Node2: BEGIN; B += 100; ghi WAL rieng (chua commit)
t3 Node2: CHET (mat dien) TRUOC khi COMMIT
t4 Node2 song lai: WAL node2 KHONG co dau "committed"
-> rollback phan do cua RIENG no -> B khong doi
Ket qua: A: -100 (da tru, ben) | B: +0 (khong cong)
-> mot NUA giao dich da hieu luc -> mat 100, vi pham "tong khong doi"
Lần theo cơ chế: ở t4, WAL của node 2 làm đúng việc của nó — thấy không có dấu "committed" nên rollback phần dở của chính node 2. Nhưng đó là phục hồi cục bộ. Không có một WAL chung hay một lock manager chung nào bắc qua cả hai node để thấy bức tranh "node 1 đã commit, node 2 fail → phải cuộn lại node 1". Còn node 1, từ góc nhìn của nó, giao dịch đã thành công và bền — nó không có lý do gì tự revert. Không ai giữ bức tranh toàn cục để kích hoạt rollback — chính là "điểm điều phối trung tâm" đã biến mất. Đây là cơ chế gốc của mọi thứ còn lại trong module: để cuộn lại được, các node phải thoả thuận trước khi bất kỳ ai commit (2PC, bài 03), hoặc bù trừ sau khi đã commit (saga, bài 04).
flowchart TB T["Chuyen tien A -> B (2 node)"] --> N1["Node 1: A -= 100<br/>WAL rieng -> COMMIT ok (ben)"] T --> N2["Node 2: B += 100<br/>CHET truoc commit -> rollback cuc bo"] N1 --> R["A mat 100, B khong nhan<br/>-> khong WAL chung de cuon lai node 1"] N2 --> R
Từ mini-trace đó, hai đảm bảo vỡ theo hai hướng:
Atomicity vỡ vì mỗi node commit độc lập. Node 1 có WAL của nó, node 2 có WAL của nó. Node 1 có thể commit thành công trong khi node 2 fail — và không có "một dấu committed chung" để cả hai cùng nhìn. Cần một nghi thức để mọi node đồng ý cùng commit hoặc cùng abort, chịu được việc một node/mạng chết giữa chừng. Đó là two-phase commit (bài 03) — và nó phải cử một coordinator đóng vai "người báo chốt", kéo theo điểm yếu blocking.
Isolation vỡ vì không có lock manager chung. Hai giao dịch đụng dữ liệu ở các node khác nhau; không manager đơn lẻ nào thấy toàn cảnh để ngăn chúng giẫm chân. Bài 02 ôn lại các anomaly (lost update, write skew) ở mức single-node làm nền — vì khi phân tán, đảm bảo "serializable" (như thể tuần tự) còn khó hơn nhiều, và đó không phải thứ 2PC (bài 03) giải được (2PC chỉ lo atomicity, không lo isolation xuyên node).
Trong ACID, Consistency nghĩa là "giao dịch giữ các ràng buộc nghiệp vụ đúng" (số dư không âm, tổng tiền không đổi) — nhưng đó là trách nhiệm của ứng dụng (viết đúng logic), không phải thứ database tự bảo đảm. Database cấp atomicity + isolation làm công cụ để ứng dụng đạt consistency. (Đừng nhầm chữ C này với "consistency" trong CAP hay trong replication — ba thứ tên giống nhau nhưng khác nghĩa; Module 6 sẽ tách bạch.)
4. Pitfall — bê nguyên tư duy single-node transaction lên nhiều node
Sai lầm: giả định cứ mở một transaction bao quanh vài thao tác ở các service/DB khác nhau là chúng atomic — như trên một node.
❌ Chuyen tien: node1.trUpdate(A -= 100); node2.trUpdate(B += 100);
xem nhu "atomic" vi co goi trong 1 khoi code
-> node1 commit xong, node2 mat mang -> A mat 100, B khong nhan -> tien bay
-> khong co WAL chung -> khong co all-or-nothing that su
❌ Tuong isolation tu dong dung xuyen service vi "moi service co transaction"
-> hai giao dich o hai node van co the tao anomaly (bai 02)
Hệ quả: mất tính atomic im lặng — code trông như giao dịch nhưng thực chất là hai thao tác độc lập có thể chỉ một cái thành công.
Hướng đúng: nhận ra atomicity/isolation xuyên node không miễn phí — nó cần một cơ chế tường minh (2PC) đắt và mong manh, hoặc một thiết kế tránh nó (saga, bài 04). Trước khi viết "transaction xuyên node", hỏi: thao tác này có thật cần atomic xuyên node, hay có thể tách thành các bước bù trừ được? Rất thường là được — và rẻ hơn nhiều.
5. 📚 Deep Dive
- Designing Data-Intensive Applications (Kleppmann) — Chương 7 "Transactions" — ACID chính xác nghĩa gì, vì sao "Consistency" là trách nhiệm ứng dụng, và vì sao atomicity/isolation khó khi phân tán.
- Khoá SQL của OLHub (module Transactions & consistency) là nền single-node: ACID, isolation levels, MVCC — bài này mở rộng đúng các khái niệm đó ra nhiều node.
Ghi chú: DDIA Ch7 nhấn: nhiều "đảm bảo" của transaction là công cụ giảm tải suy luận cho lập trình viên; khi phân tán, cái giá của các đảm bảo đó tăng vọt — nên phải cân nhắc có thật cần không.
6. Liên hệ các bài khác
- Bài 02 — Anomaly & serializability: đào sâu phần isolation — các anomaly cụ thể và chuẩn serializable.
- Bài 03 — 2PC: cơ chế cho atomicity xuyên node — và vì sao coordinator làm nó blocking.
- Module 2 — Replication: dữ liệu trải nhiều node (do replication + partitioning) là lý do transaction phải xuyên node.
- Module 6 — CAP đúng nghĩa: ba chữ "consistency" (ACID-C, CAP-C, replication) khác nghĩa — sẽ tách bạch ở đó.
7. Tóm tắt
- Transaction = atomicity (cùng commit/cùng huỷ) + isolation (đồng thời như tuần tự); trên một node do WAL + lock/MVCC của cùng một node lo.
- Cả hai dựa vào một điểm điều phối trung tâm — thứ biến mất khi dữ liệu trải nhiều node.
- Atomicity xuyên node: mỗi node commit độc lập → cần nghi thức "cùng quyết" chịu lỗi giữa chừng → 2PC (bài 03).
- Isolation xuyên node: không có lock manager chung → anomaly xuyên node, serializable khó hơn (bài 02).
- Consistency (chữ C) là trách nhiệm ứng dụng; database cấp atomicity + isolation làm công cụ.
- Anti-pattern: bê nguyên "BEGIN...COMMIT là atomic" lên nhiều node → mất tính atomic im lặng.
8. Tự kiểm tra
Q1Phân biệt atomicity và isolation. Trên một node, cơ chế nào đảm bảo mỗi cái?▸
Atomicity: mọi thao tác trong một giao dịch cùng có hiệu lực hoặc không cái nào (all-or-nothing); lỗi giữa chừng thì rollback. Isolation: nhiều giao dịch đồng thời cho kết quả như thể chạy lần lượt (không giẫm chân nhau).
Trên một node: atomicity (và durability) dựa vào write-ahead log — ghi ý định trước, commit = một dấu "committed"; crash trước dấu đó thì rollback. Isolation dựa vào lock hoặc MVCC, do một manager duy nhất trên node điều phối. Điểm chung: cả hai đứng trên cùng một nền tảng của node — thứ sẽ biến mất khi dữ liệu trải nhiều node (xem phần sau).
Q2Vì sao atomicity 'vỡ' khi giao dịch trải trên nhiều node? Điều gì thiếu so với single-node?▸
Vì mỗi node có WAL riêng và commit độc lập: node 1 có thể commit thành công trong khi node 2 fail, và không có "một dấu committed chung" nào để cả hai cùng nhìn. Thao tác trở thành nửa vời (trừ A mà không cộng B).
Cái thiếu so với single-node là điểm điều phối trung tâm (một WAL, một lock manager của cùng node). Phải bịa ra một nghi thức để mọi node đồng ý cùng commit hoặc cùng abort, và chịu được việc một node/mạng chết giữa chừng — đó là 2PC (bài 03), kèm điểm yếu coordinator.
Q3Trong chuyển tiền A→B với A ở node 1, B ở node 2: kịch bản nào làm mất tính atomic nếu chỉ 'gọi hai update trong một khối code'?▸
Kịch bản: node1 thực hiện A -= 100 và commit thành công; ngay sau đó node2 mất mạng/chết trước khi commit B += 100. Kết quả: A đã mất 100, B chưa nhận — tiền bốc hơi, dữ liệu vi phạm ràng buộc "tổng không đổi".
Nguyên nhân: gọi hai update trong một khối code chỉ tạo cảm giác "một giao dịch", nhưng thực chất là hai giao dịch độc lập ở hai node — không có WAL chung, không có all-or-nothing thật. Muốn atomic xuyên node phải có cơ chế tường minh (2PC) hoặc thiết kế bù trừ (saga).
Q4Vì sao chữ 'C' (Consistency) trong ACID không phải thứ database tự đảm bảo, và nó liên quan thế nào tới atomicity + isolation?▸
Vì Consistency ở đây nghĩa là "giao dịch giữ đúng các ràng buộc nghiệp vụ" (số dư không âm, tổng tiền không đổi) — mà những ràng buộc đó do ứng dụng định nghĩa và viết đúng logic. Database không biết "số dư không được âm" trừ khi bạn khai; nó không tự bịa ra tính đúng nghiệp vụ.
Database cung cấp atomicity + isolation làm công cụ: nhờ all-or-nothing và không-giẫm-chân, ứng dụng có thể giữ ràng buộc đúng. Nhưng nếu code nghiệp vụ sai, database vẫn commit một trạng thái "consistent về mặt kỹ thuật" mà sai nghiệp vụ. (Lưu ý: chữ C này khác "consistency" trong CAP và replication.)
Q5Trước khi viết một 'transaction xuyên node', câu hỏi nào nên hỏi, và vì sao?▸
Nên hỏi: thao tác này có THẬT SỰ cần atomic xuyên node, hay có thể tách thành các bước bù trừ được?
Vì atomicity/isolation xuyên node không miễn phí — cách "đúng" (2PC) vừa đắt (nhiều round-trip, giữ khoá lâu) vừa mong manh (coordinator chết → blocking). Rất nhiều bài toán tưởng cần atomic thật ra chấp nhận được "eventual + bù trừ" (saga): làm từng bước, nếu bước sau fail thì chạy hành động bù cho bước trước. Hỏi câu này trước giúp tránh gánh cái giá của 2PC khi không cần — đúng kết luận thực dụng của module.
Bài tiếp theo: Anomaly & serializability — lost update, write skew, SSI
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên