CAP Theorem — Vì sao hệ thống phân tán luôn phải chọn 2 trong 3?
CAP (hay định lý Brewer) là nền tảng của mọi hệ thống phân tán, nhưng em thấy nhiều người hiểu nó quá đơn giản thành thi tam giác chọn 2. Định lý thật ra nói ít hơn thế, mà hậu quả nhiều hơn thế. Bài này em gỡ rối lại để áp dụng được vào thiết kế thực tế.
Định lý nói gì thực ra
Khi một hệ thống bị chia mạng (partition — hai node không liên lạc được nhau), ta phải chọn giữa:
- Consistency (C): mọi client nhìn thấy cùng một dữ liệu, node nào cũng trả lời đúng nhất.
- Availability (A): mọi request đều được đáp ứng, kể cả khi dữ liệu có thể hơi cũ.
Điểm mấu chốt: khi network PARTITION (thường xảy ra hơn người ta tưởng, chỉ vì một do network hiccup), bạn KHÔNG thể có cả C lẫn A. Nút bị cô lập không biết phía bên kia đã ghi gì — bạn hoặc trả lời bản cũ (hy sinh C), hoặc từ chối/treo (hy sinh A). Đây không phải lựa chọn, mà là bắt buộc.
Khi mạng khoẻ (không partition), bạn vẫn có đủ C + A — tam giác chỉ là cái bẫy tư duy khi đọc nhầm thành luôn chọn 2.
Quyết định trong thực tế
Trong 7 năm làm backend, em gần như chưa từng gặp hệ thống thuần CA hay AP. Thực tế là mỗi request/entity chọn cách xử lý khác nhau. Công thức em hay xài:
- Dữ liệu đếm tiền, số dư, đơn hàng → ưu tiên C, chấp nhận từ chối khi không chắc chắn (số dư không được âm).
- Dữ liệu feed, số lượt xem, profile → ưu tiên A, chấp nhận eventual consistency (xem hơi cũ cũng ok).
Ví dụ: hệ thống order, khi thanh toán server của bank bị chậm, ta KHÔNG trả "thành công" cho client rồi giải quyết sau — vì sau đó phát hiện thiếu tiền thì khổ. Thay vào đó trả "đang xử lý", bảo người dùng chờ, rồi ghi chắc chắn khi có kết quả. Đó chính là chọn C hơn A.
PACELC — mở rộng thực dụng hơn
CAP chỉ nói về khi partition xảy ra. Còn PACELC mới là thứ giúp thiết kế hằng ngày: If Partition → chọn A hay C; Else (network khoẻ) → chọn Latency hay Consistency.
PACELC: If P, choose A/C; Else, choose L/C
- Cassandra, DynamoDB: AP + EL (khi khoẻ ưu tiên latency thấp, consistency có thể giảm).
- CockroachDB, Spanner: CP + EC (luôn ưu tiên consistency, trả giá latency).
- PostgreSQL standalone: CA + LC (đơn node, nhất quán mạnh, chấp nhận chậm khi ghi).
Code: một vụ ghi AP dễ dính bug
Dạng bug kinh điển khi lầm tưởng mình đang AP mà vẫn cần C:
// Nguy hiểm: đọc rồi ghi trong môi trường eventual consistency
func transfer(from, to string, amount int) error {
bal := db.ReadBalance(from) // có thể là bản cũ!
if bal < amount {
return errors.New("không đủ tiền") // quyết định trên dữ liệu cũ
}
return db.Debit(from, amount) // rồi mới trừ
}
Khi read đến từ replica trễ hơn node ghi, hai request đồng thời cả hai thấy "đủ tiền" và cả hai đều trừ — số dư thành âm. Đây là lý do dữ liệu tiền phải đi qua atomic compare-and-set hoặc lock ở leader, chứ không dựa trên đọc rồi ghi:
// An toàn với CAS — atomic, đúng trong cả chế độ AP
updated := db.UpdateWhere(
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
amount, from, amount,
)
if updated.RowsAffected() == 0 {
return errors.New("không đủ tiền")
}
Kinh nghiệm thực chiến
Em từng dính trận: team chọn MongoDB (theo mặc định là AP/eventual) cho module số dư ví, vì "nhanh, dễ scale". Kết quả là một loạt báo cáo sai số dư giữa replica khi có momentary partition. Phải chuyển logic ghi sang leader + CAS / transaction mới ổn. Bài học: chọn cơ sở dữ liệu theo yêu cầu nhất quán của DỮ LIỆU, không theo độ hot của công nghệ.
Tóm lại
- CAP không cho bạn "chọn 2 trong 3" tự do — nó là ràng buộc chỉ khi partition, và bạn phải chọn C hay A.
- Phân loại dữ liệu theo mức quan trọng nhất quán, rồi chọn DB + strategy phù hợp cho từng loại.
- Đọc thêm PACELC (áp dụng cho lúc mạng khoẻ) để tinh chỉnh trade-off latency vs consistency.
- Dữ liệu tiền bạc: luôn atomic compare-and-set, đừng đọc-rồi-ghi trên dữ liệu eventual.
Chia sẻ: anh gặp case nào mà chọn A rồi hối vì mất dữ liệu không? Góc dưới bình luận cho em biết nhé!