Software Architecture

CAP Theorem: Tại sao bạn không bao giờ có được cả ba (Consistency, Availability, Partition Tolerance)?

7/19/2026 · 12p đọc


title: "CAP Theorem: Tại sao bạn không bao giờ có được cả ba (Consistency, Availability, Partition Tolerance)?"
series: "Software Architecture Masterclass"
part: "Phần 3 — Hệ thống Phân tán & Hạ tầng"
order: 21
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags: ["CAP Theorem", "distributed systems", "consistency", "availability", "partition tolerance", "system design"]

CAP Theorem: Tại sao bạn không bao giờ có được cả ba (Consistency, Availability, Partition Tolerance)?

Một buổi họp kick-off dự án, sếp sản phẩm nói: "Hệ thống ví điện tử mới phải luôn đúng số dư — không được sai một đồng. Đồng thời phải luôn sẵn sàng 24/7, không được downtime kể cả khi một data center gặp sự cố mạng. Và tất nhiên phải chạy multi-region để chịu được thiên tai." Cả ba yêu cầu đều hợp lý khi nói riêng lẻ. Nhưng khi ghép lại thành một câu, bạn — với tư cách kiến trúc sư — đang bị giao một bài toán không có lời giải.

Đây không phải vấn đề về năng lực kỹ thuật của đội ngũ, không phải chuyện "chưa đủ giỏi để làm được cả ba". Đây là một giới hạn toán học đã được chứng minh: trong một hệ phân tán có phân vùng mạng (network partition), bạn buộc phải chọn giữa Consistency (nhất quán) và Availability (sẵn sàng). Không có lựa chọn thứ ba. Vấn đề không nằm ở việc "làm sao để có cả ba" — mà nằm ở việc rất nhiều team không biết mình đang âm thầm chọn phe nào, cho tới khi một sự cố mạng thực sự xảy ra và hệ thống hành xử theo cách không ai lường trước.

Bài này mổ xẻ CAP theorem không phải như một định lý hàn lâm để trích dẫn trong slide, mà như một công cụ ra quyết định: với từng service trong hệ thống của bạn, khi mạng đứt, nó nên trả lỗi hay trả dữ liệu cũ? Câu trả lời đó phải được quyết định trước, bằng thiết kế — không phải để ngẫu nhiên xảy ra trong incident lúc 2 giờ sáng.

The Problem

Một hệ thống phân tán, theo định nghĩa, chạy trên nhiều node — có thể là nhiều server trong cùng một data center, hoặc nhiều region cách nhau hàng nghìn km. Giữa các node đó là mạng, và mạng thì không hoàn hảo: cáp quang bị đứt, router lỗi, độ trễ tăng đột biến, hoặc đơn giản là gói tin bị rớt. Khi điều đó xảy ra, node A không còn "nói chuyện" được với node B nữa. Đây gọi là network partition — phân vùng mạng.

Câu hỏi kiến trúc bây giờ là: khi node A nhận một request ghi/đọc dữ liệu nhưng không liên lạc được với node B để đồng bộ, node A nên làm gì?

  • Từ chối xử lý request, trả lỗi cho client, chờ tới khi mạng thông trở lại để đảm bảo dữ liệu không bao giờ bất nhất giữa A và B.
  • Hoặc tự xử lý request bằng dữ liệu cục bộ nó có, chấp nhận rằng A và B có thể đang "nghĩ" hai giá trị khác nhau tại cùng một thời điểm.

Không có lựa chọn thứ ba nào cho phép vừa trả lời request (available) vừa đảm bảo A và B luôn đồng nhất tuyệt đối (consistent) trong lúc đường dây giữa chúng bị cắt. Đây chính là nội dung của CAP theorem, do Eric Brewer phát biểu năm 2000 và được Seth Gilbert, Nancy Lynch chứng minh hình thức năm 2002.

Vấn đề thực tế không phải là hiểu định lý — phần lớn kỹ sư đã nghe qua CAP ít nhất một lần. Vấn đề là: khi thiết kế một service cụ thể (giỏ hàng, số dư ví, feed mạng xã hội, inventory kho), team thường không ngồi xuống trả lời câu hỏi "khi phân vùng xảy ra, service này nghiêng về C hay A?" một cách tường minh. Hệ quả là hành vi của hệ thống lúc sự cố phụ thuộc vào cấu hình mặc định của database được chọn — một quyết định kiến trúc quan trọng bị ủy quyền ngẫu nhiên cho default config của vendor.

The Theory

CAP theorem phát biểu: trong một hệ thống phân tán, bạn chỉ có thể đảm bảo tối đa 2 trong 3 tính chất sau tại cùng một thời điểm:

  • Consistency (C) — mọi node đọc dữ liệu đều thấy cùng một giá trị mới nhất (linearizability). Đọc ngay sau khi ghi, ở bất kỳ node nào, đều trả về giá trị vừa ghi.
  • Availability (A) — mọi request tới một node còn sống đều nhận được phản hồi (không phải lỗi), dù không đảm bảo đó là dữ liệu mới nhất.
  • Partition Tolerance (P) — hệ thống tiếp tục hoạt động dù mạng giữa các node bị cắt/mất gói tin.

Điểm mấu chốt hay bị hiểu sai: P không phải một lựa chọn thiết kế — nó là một thực tế không thể tránh khỏi của bất kỳ hệ phân tán thật nào chạy trên mạng vật lý. Bạn không "chọn" có chịu partition hay không; mạng sẽ đứt bất kể bạn muốn hay không. Vì vậy CAP không thực sự là "chọn 2 trong 3" theo nghĩa toán tổ hợp — nó là: khi P xảy ra (và nó sẽ xảy ra), bạn buộc phải chọn giữa C và A. CP hay AP không phải là thuộc tính tĩnh của một database — nó là hành vi của hệ thống tại đúng khoảnh khắc network partition diễn ra.

Nói cách khác:

  • Nếu không có partition (mạng khỏe mạnh, mọi node liên lạc bình thường), bạn có thể có cả C và A cùng lúc — đây là trạng thái vận hành bình thường của hầu hết hệ thống hầu hết thời gian.
  • Chỉ khi partition thực sự xảy ra, CAP mới "kích hoạt" và buộc bạn chọn: hệ CP sẽ từ chối phục vụ (hoặc trả lỗi) ở phía không có quorum/không liên lạc được để giữ tính đúng đắn của dữ liệu; hệ AP sẽ tiếp tục trả lời bằng dữ liệu nó có, chấp nhận rủi ro dữ liệu bị phân kỳ (divergent) tạm thời, và xử lý hội tụ (reconciliation) sau khi mạng thông trở lại — thường qua các kỹ thuật eventual consistency, vector clocks, hoặc conflict resolution (last-write-wins, CRDT, application-level merge).

Một điểm cần làm rõ: CAP nói về hành vi khi có partition, không phải về latency hay hiệu năng thông thường. PACELC là phần mở rộng thực tế hơn của CAP, chỉ ra rằng ngay cả khi không có Partition (P), bạn vẫn phải đánh đổi giữa Latency (L) và Consistency (C) — vì đồng bộ hóa mạnh giữa các node luôn tốn thời gian mạng. Điều này giải thích tại sao ngay cả các hệ CP "khỏe mạnh" cũng thường có latency cao hơn hệ AP trong điều kiện vận hành bình thường, không chỉ lúc sự cố.

Quan trọng: CAP áp dụng ở cấp độ hệ thống phân tán có nhiều bản sao dữ liệu (replicated state) qua mạng — không áp dụng cho một database đơn instance không replicate. Và trong thực tế, một hệ thống lớn không phải "CP hay AP" một cách tuyệt đối cho toàn bộ hệ thống — quyết định này nên được đưa ra ở cấp độ từng service/từng loại dữ liệu, vì các domain khác nhau trong cùng một sản phẩm có yêu cầu khác nhau.

The Trade-off

Đây là phần phải nói rõ ràng, không được mơ hồ.

Chọn CP (Consistency + Partition Tolerance, hy sinh Availability)

Được gì: dữ liệu luôn đúng, luôn nhất quán trên mọi node đọc được. Không bao giờ có chuyện hai client đọc cùng một key nhưng thấy hai giá trị khác nhau. Đây là yêu cầu bắt buộc cho các domain mà đọc sai dữ liệu gây hậu quả tài chính/pháp lý trực tiếp: sổ cái kế toán, số dư tài khoản, kiểm tra tồn kho trước khi trừ tiền, hệ thống đặt vé/đặt chỗ không được overbook.

Mất gì — cụ thể: khi phân vùng mạng xảy ra (node/region mất kết nối tới quorum), hệ thống sẽ từ chối phục vụ ở phía thiểu số (không đạt quorum) — trả lỗi 503/timeout thay vì trả dữ liệu có thể sai. Nghĩa là user ở region bị cô lập không đặt được đơn hàng, không xem được số dư, dù dữ liệu vẫn nằm đó trên đĩa, chỉ là node không dám trả lời vì không chắc nó là bản mới nhất. Chi phí kinh doanh là downtime cục bộ thực sự, đo được bằng phút mất doanh thu/giao dịch bị chặn. Ví dụ thực tế: RDBMS cluster nhất quán mạnh (PostgreSQL với synchronous replication, hoặc các hệ dùng Raft/Paxos consensus như etcd, Google Spanner ở chế độ strong consistency) sẽ dừng ghi hoặc dừng đọc ở node thiểu số cho tới khi quorum được khôi phục.

Chọn AP (Availability + Partition Tolerance, hy sinh Consistency)

Được gì: hệ thống luôn trả lời, ở mọi node, kể cả khi bị cô lập hoàn toàn khỏi phần còn lại của cluster. Trải nghiệm người dùng không bao giờ thấy lỗi "hệ thống tạm thời không khả dụng" — feed vẫn load, giỏ hàng vẫn thêm được, like vẫn bấm được.

Mất gì — cụ thể: dữ liệu đọc được có thể (stale) so với bản ghi mới nhất đang tồn tại ở node khác — đây không phải rủi ro lý thuyết, mà là hành vi được thiết kế chủ động. Tệ hơn, nếu cùng một key bị ghi ở hai node bị cô lập với nhau trong lúc partition, khi mạng nối lại bạn có write conflict thật sự cần giải quyết: last-write-wins có thể làm mất một trong hai ghi đè hợp lệ, merge logic tự viết tốn công sức và dễ có bug tinh vi. Ví dụ thực tế: DynamoDB (ở chế độ eventually consistent read, vốn là mặc định để tối ưu chi phí/độ trễ), Cassandra với consistency level thấp, hoặc bất kỳ hệ dùng gossip protocol nào đều chấp nhận đánh đổi này để đổi lấy availability gần như tuyệt đối.

Điểm mấu chốt của phần trade-off: không có phương án nào miễn phí. Chọn CP tức là bạn ký cam kết ngầm với business rằng "đôi khi hệ thống sẽ từ chối phục vụ, và đó là tính năng chứ không phải lỗi". Chọn AP tức là bạn ký cam kết ngầm rằng "đôi khi người dùng sẽ thấy dữ liệu cũ hoặc gặp conflict cần resolve, và đó cũng là tính năng chứ không phải lỗi". Sai lầm phổ biến nhất không phải là chọn sai giữa CP và AP, mà là không chọn — để mặc định của database quyết định thay, rồi ngạc nhiên khi sự cố xảy ra.

The Architecture Diagram

graph TD
    CAP["CAP Triangle<br/>Chọn tối đa 2/3 khi có Partition"]

    CAP --- C["Consistency<br/>(mọi node thấy cùng dữ liệu)"]
    CAP --- A["Availability<br/>(luôn phản hồi request)"]
    CAP --- P["Partition Tolerance<br/>(chịu được mạng đứt — bắt buộc<br/>với hệ phân tán thật, không phải lựa chọn)"]

    C --- CP["Góc CP<br/>Hy sinh Availability<br/>Ví dụ: PostgreSQL cluster (sync replication),<br/>etcd, Google Spanner (strong mode),<br/>MongoDB (majority write concern)<br/>→ Sổ cái tài chính, số dư ví,<br/>hệ thống đặt chỗ/vé"]

    A --- AP["Góc AP<br/>Hy sinh Consistency<br/>Ví dụ: DynamoDB (eventual read),<br/>Cassandra, CouchDB, gossip-based systems<br/>→ Newsfeed, like/comment count,<br/>session cache, product catalog view"]

    P --- CA["Góc CA (lý thuyết)<br/>Chỉ tồn tại khi KHÔNG có partition<br/>— tức hệ thống single-node<br/>hoặc mạng LAN hoàn hảo<br/>Không thực tế cho hệ phân tán thật"]

    style CAP fill:#2d3748,stroke:#4a5568,color:#fff
    style CP fill:#742a2a,stroke:#c53030,color:#fff
    style AP fill:#22543d,stroke:#38a169,color:#fff
    style CA fill:#4a5568,stroke:#718096,color:#fff,stroke-dasharray: 5 5
sequenceDiagram
    participant Client
    participant NodeA as Node A (region 1)
    participant NodeB as Node B (region 2)

    Note over NodeA,NodeB: Mạng hoạt động bình thường — cả C và A đều đạt được
    Client->>NodeA: Write balance = 100
    NodeA->>NodeB: Replicate (sync)
    NodeB-->>NodeA: ACK
    NodeA-->>Client: 200 OK

    Note over NodeA,NodeB: NETWORK PARTITION xảy ra
    rect rgb(90, 40, 40)
    Note over NodeA,NodeB: Kịch bản CP: Node A không đạt quorum → từ chối
    Client->>NodeA: Write balance = 150
    NodeA--xNodeB: Không liên lạc được
    NodeA-->>Client: 503 Service Unavailable
    end

    rect rgb(30, 70, 45)
    Note over NodeA,NodeB: Kịch bản AP: Node A vẫn phục vụ bằng dữ liệu cục bộ
    Client->>NodeA: Write balance = 150
    NodeA--xNodeB: Không liên lạc được
    NodeA-->>Client: 200 OK (đã ghi cục bộ, chưa đồng bộ)
    Note over NodeA,NodeB: Khi mạng nối lại: reconciliation/conflict resolution
    end

Implementation Guardrails

  1. Quyết định CP/AP tường minh ở cấp độ từng service, viết thành ADR — không để mặc định driver/DB quyết định thay. Với mỗi service lưu trạng thái, bắt buộc trả lời câu hỏi "khi partition xảy ra, service này nên trả lỗi hay trả dữ liệu cũ?" trước khi chọn công nghệ lưu trữ, và ghi quyết định đó vào Architecture Decision Record (xem Bài 6). Không được để câu trả lời là "tùy config mặc định của MongoDB/DynamoDB lúc đó là gì".

  2. Domain tài chính/giao dịch không đảo ngược được (ledger, số dư, thanh toán, kiểm tra tồn kho trước khi trừ) bắt buộc đi CP — chấp nhận trả lỗi/timeout khi phân vùng, không bao giờ chấp nhận đọc số dư/tồn kho stale rồi cho phép giao dịch tiếp tục dựa trên số liệu sai. Nếu cần availability cao hơn cho domain này, giải pháp đúng là giảm bán kính blast radius của partition (multi-AZ trong 1 region với latency thấp, quorum 3-5 node) chứ không phải chuyển sang AP.

  3. Domain trải nghiệm người dùng không nhạy cảm với độ trễ dữ liệu (feed, like/view count, gợi ý, cache session, catalog hiển thị) nên đi AP — và bắt buộc thiết kế cơ chế conflict resolution rõ ràng (last-write-wins với timestamp, CRDT, hoặc merge theo business logic) thay vì để hệ thống tự "đoán" cách hợp nhất dữ liệu phân kỳ. Đừng chọn AP rồi không có kế hoạch gì cho write conflict — đó là AP nửa vời, nguy hiểm hơn cả CP.

  4. Test hành vi khi partition bằng chaos engineering thật (network fault injection), không chỉ đọc tài liệu vendor. Behavior thực tế của một cluster PostgreSQL, MongoDB, hay DynamoDB lúc split-brain phụ thuộc vào cấu hình cụ thể (write concern, quorum size, consistency level) — cùng một sản phẩm database có thể ứng xử CP hoặc AP tùy config. Phải mô phỏng partition trong staging/pre-prod (cắt mạng giữa AZ, tăng latency giả lập) để xác nhận hệ thống hành xử đúng như thiết kế đã quyết định ở guardrail 1, trước khi nó xảy ra thật trong production.

Architect's Verdict

CAP không phải một checklist kiểm tra xem hệ thống "có đủ 3 tính chất" hay không — nó là một cái giá bắt buộc phải trả khi mạng đứt, và mạng luôn đứt sớm hay muộn. Việc của kiến trúc sư không phải là tìm cách lách CAP, mà là biết chính xác từng phần trong hệ thống của mình sẽ hành xử thế nào tại khoảnh khắc đó, và đảm bảo hành vi đó khớp với kỳ vọng của business — chứ không phải khớp với config mặc định của database team đã chọn hai năm trước. Nếu bạn không thể trả lời câu hỏi "service X của tôi là CP hay AP khi partition xảy ra" trong vòng 10 giây, đó là một khoảng trống kiến trúc cần đóng ngay, trước khi incident đóng giúp bạn theo cách đau đớn hơn.

🔗 Bài viết liên quan: Data Consistency — Eventual Consistency, High Availability — Design for Failure


Bài trước: API Gateway & Service Mesh · Bài tiếp theo: Data Consistency

CAP Theorem: Tại sao bạn không bao giờ có được cả ba (Consistency, Availability, Partition Tolerance)?