Skip to content

Nhất quán & CAP ​

Khi dữ liệu có nhiều bản sao, “đọc ra giá trị mới nhất” không còn hiển nhiên. Trang này là trang chính về các mô hình nhất quán (consistency model), tức là những lời hứa của hệ thống về việc client được thấy gì, cùng hai cách nói hay gặp trong phỏng vấn là CAP và PACELC. Cơ chế replication, ba kiểu đọc sai khi có replication lag và quorum đã có ở trang Replication, nên trang này chỉ dẫn link sang. Phần đo thật dùng semi-sync replication của MySQL trong lab/nhat-quan/.

“Nhất quán” có ít nhất ba nghĩa ​

Cùng một chữ consistency, nhưng ba ngữ cảnh khác nhau nói về ba chuyện khác nhau:

Ngữ cảnhNghĩaVí dụ
Chữ C trong ACIDDữ liệu luôn thoả các ràng buộc của ứng dụngTổng tiền các tài khoản không đổi sau khi chuyển khoản, khoá ngoại không trỏ vào dòng không tồn tại
Mô hình nhất quán của hệ có nhiều bản saoClient đọc thì được thấy những giá trị nàoĐọc từ replica có thể thấy dữ liệu cũ
Chữ C trong CAPĐúng một mô hình cụ thể là linearizableMục CAP bên dưới

Một cặp khái niệm khác cũng hay bị nhầm là serializable và linearizable. Serializable là mức isolation của transaction: nhiều transaction chạy đồng thời cho kết quả như thể chạy lần lượt theo một thứ tự nào đó, nhưng thứ tự đó không cần khớp với thời gian thật (Transaction & isolation). Linearizable là lời hứa trên một object (một key, một dòng): mọi thao tác trông như xảy ra tức thời tại một điểm nằm giữa lúc gửi và lúc nhận trả lời, theo đúng thứ tự thời gian thật. Gộp cả hai lại thành strict serializable, và Spanner gọi mức này là external consistency.

Các mô hình nhất quán, từ mạnh tới yếu ​

Linearizable ​

Theo Jepsen, linearizable nghĩa là mọi thao tác trông như xảy ra nguyên tử, theo một thứ tự khớp với thời gian thật: nếu thao tác A xong trước khi thao tác B bắt đầu, thì B phải có hiệu lực sau A. Nói cách khác, hệ thống cư xử như thể dữ liệu chỉ có một bản. Một khi có client đọc được giá trị mới, mọi lần đọc bắt đầu sau đó, của bất kỳ client nào, cũng phải thấy giá trị đó hoặc mới hơn.

Distributed lock, bầu leader, kiểm tra trùng username hay số dư tài khoản đều cần mức này (Distributed lock).

Sequential ​

Sequential consistency yếu hơn một bậc: vẫn phải có một thứ tự chung mà mọi client cùng thấy, và thứ tự đó giữ đúng thứ tự các thao tác của từng client. Nhưng thứ tự giữa các client khác nhau không cần khớp với thời gian thật. Một lần đọc bắt đầu sau khi lệnh ghi đã xong vẫn được phép trả giá trị cũ, miễn là mọi client cùng thấy một thứ tự.

Thử với các lịch sử dưới đây. Bấm vào một lần đọc để đổi giá trị nó trả về. Mô phỏng thử mọi thứ tự có thể để xem lịch sử đó có thuộc mô hình nào:

Lịch sử

Mỗi thanh là một thao tác trên biến x (ban đầu bằng 0), từ lúc client gửi tới lúc nhận trả lời. Bấm vào một lần đọc để đổi giá trị nó trả về. Lệnh ghi của A kéo dài, nên đọc trong lúc đó thấy 0 hay 1 đều được. Nhưng B đã thấy 1, mà C bắt đầu sau khi B xong lại thấy 0.

ABCghi 1đọc → 1đọc → 00246810
Đổi giá trị đọc
Không linearizable

Không có thứ tự nào vừa khớp mọi giá trị đọc được, vừa giữ quy tắc: thao tác nào xong trước khi thao tác kia bắt đầu thì phải đứng trước.

Sequential consistent

Một thứ tự hợp lệ, chỉ giữ thứ tự trong từng client: C: đọc → 0 → A: ghi 1 → B: đọc → 1.

Lịch sử “Hai lệnh ghi đồng thời” là chỗ khác nhau rõ nhất. C đọc ra 2 rồi ra 1 sau khi cả hai lệnh ghi đã xong. Với sequential thì vẫn được, vì có thể xếp “ghi 2, C đọc 2, ghi 1, C đọc 1”; thứ tự này giữ đúng thứ tự trong từng client. Với linearizable thì không, vì lệnh “ghi 1” đã xong trước khi C bắt đầu đọc, nên không thể xếp nó vào giữa hai lần đọc của C.

Causal ​

Causal consistency chỉ đòi hỏi các thao tác có quan hệ nhân quả phải hiện ra theo cùng một thứ tự ở mọi nơi. Các thao tác độc lập với nhau thì mỗi client có thể thấy theo thứ tự khác nhau. Quan hệ nhân quả gồm: thao tác trước và sau của cùng một client, và một lệnh ghi được thực hiện sau khi đã đọc kết quả của một lệnh ghi khác.

Ví dụ ở phần bình luận: A đăng câu hỏi, B đọc câu hỏi rồi trả lời. Câu trả lời phụ thuộc câu hỏi, nên không ai được thấy câu trả lời mà không thấy câu hỏi. Còn hai bình luận độc lập của C và D thì hiện theo thứ tự nào cũng được. Đây chính là kiểu đọc sai “thấy câu trả lời trước câu hỏi” ở trang Replication.

Các đảm bảo theo phiên ​

Bốn đảm bảo sau chỉ hứa với từng client (từng phiên), không hứa gì giữa các client:

Đảm bảoNghĩa
Read your writesClient luôn đọc được những gì chính nó đã ghi
Monotonic readsClient không bao giờ đọc lùi về giá trị cũ hơn giá trị nó đã đọc
Monotonic writesCác lệnh ghi của một client được áp dụng đúng thứ tự client gửi
Writes follow readsLệnh ghi của client được xếp sau những gì client đó đã đọc trước khi ghi

Cách làm read your writes và monotonic reads khi đọc từ replica có ở Replication và Tách đọc / ghi.

Eventual ​

Eventual consistency chỉ hứa rằng nếu ngừng ghi thì sau một lúc mọi bản sao sẽ giống nhau. Nó không nói gì về việc trước đó client đọc được gì, cũng không nói “một lúc” là bao lâu.

Mô hình nào còn chạy được khi mạng bị chia cắt ​

Jepsen chia các mô hình theo việc chúng có còn chạy được khi mạng bị chia cắt hay không:

NhómMô hìnhKhi mạng bị chia cắt
Không chạy đượcStrict serializable, serializable, snapshot isolation, repeatable read, linearizable, sequentialMột số hoặc mọi node phải ngừng phục vụ
Sticky availableCausal, PRAM, read your writesVẫn chạy, với điều kiện client luôn nối vào cùng một node còn sống
Totally availableMonotonic reads, monotonic writes, writes follow reads, read committed, read uncommittedNode nào còn sống cũng phục vụ được

Theo thứ bậc của Jepsen, strict serializable mạnh hơn cả serializable lẫn linearizable; linearizable mạnh hơn sequential, sequential mạnh hơn causal, và causal mạnh hơn các đảm bảo theo phiên. Bảng này cũng là cách hiểu đúng nhất về CAP: mô hình càng mạnh thì càng không chạy được khi mạng bị chia cắt.

CAP ​

Gilbert và Lynch phát biểu định lý CAP như sau: trong một mạng có thể mất hoặc trễ message, không thể có dịch vụ nào vừa giữ được một biến đọc ghi linearizable (họ gọi là atomic), vừa trả lời mọi request. Ba chữ trong CAP có nghĩa hẹp hơn nhiều so với cách hay dùng:

  • C (consistency) là linearizable, đúng một mô hình trên một biến, không phải chữ C trong ACID.
  • A (availability): theo cách Kleppmann trích định nghĩa, mọi request mà một node không bị hỏng nhận được đều phải có trả lời. Một hệ mà chỉ một phần node trả lời được thì không tính là available theo nghĩa này.
  • P (partition tolerance): mạng được phép trễ hoặc mất message, và các node có thể bị chia thành những nhóm không liên lạc được với nhau.

Phần chứng minh rất ngắn. Chia các node thành hai nhóm không liên lạc được. Một client ghi giá trị mới vào nhóm thứ nhất, rồi một client khác đọc từ nhóm thứ hai. Nhóm thứ hai không có cách nào biết đã có lệnh ghi mới. Nếu nó trả lời thì trả giá trị cũ, tức mất C; nếu nó không trả lời thì mất A.

Vì sao “chọn 2 trong 3” dễ gây hiểu nhầm ​

  • P không phải là thứ được chọn. Mạng bị chia cắt là chuyện sẽ xảy ra, không phải tính năng có thể tắt đi. Một hệ “chọn CA” chỉ có nghĩa là chưa nghĩ tới chuyện gì xảy ra khi mạng bị chia cắt.
  • Việc chọn chỉ xảy ra trong lúc mạng bị chia cắt. Lúc mạng bình thường, một hệ có thể vừa linearizable vừa trả lời mọi request. Câu hỏi đúng là: khi bị chia cắt thì phía nào ngừng phục vụ, hoặc phía nào trả dữ liệu cũ.
  • Nhiều hệ không thuộc CP cũng không thuộc AP. Kleppmann lấy ví dụ single leader với async replication, cách mặc định của phần lớn database. Client bị cắt khỏi leader thì không ghi được, nên không phải A; đọc từ follower thì không linearizable, nên cũng không phải C. Ngay cả ZooKeeper, hay được xếp vào CP, cũng không cho đọc linearizable theo mặc định; phải gọi sync trước khi đọc. Vì vậy ông khuyên thôi xếp database vào ô CP hay AP, mà nói cụ thể mô hình nhất quán và hành vi khi bị chia cắt.

Thử cắt mạng và cho client hai phía cùng đọc ghi, với hai cách chọn khi bị chia cắt:

Chế độ
Mạng
N1x = 0N2x = 0N3x = 0client Xclient Y

Thử: cắt mạng, cho cả X và Y ghi rồi đọc, sau đó bấm Bình thường để nối lại. Làm lần lượt với hai chế độ.

Mô hình đơn giản hoá: chế độ CP coi như mỗi phía đa số có một leader được bầu ngay (Raft), chế độ AP gửi dữ liệu ngay cho các node cùng phía. Khi mạng bình thường, hai chế độ cho cùng kết quả; khác nhau chỉ lộ ra khi mạng bị chia cắt.

MySQL semi-sync: giữ C trong vài giây, rồi chọn A ​

Semi-sync replication của MySQL là một ví dụ CAP có thật ngay trong stack quen thuộc. Theo tài liệu MySQL, khi bật semi-sync, commit trên source phải chờ ít nhất một replica xác nhận đã ghi transaction vào relay log và flush xuống disk. Với chế độ mặc định AFTER_SYNC, source chờ xác nhận trước khi commit vào storage engine, nên trong lúc chờ các client khác chưa thấy dữ liệu mới, và failover sang replica không mất transaction đã commit. Nếu quá rpl_semi_sync_source_timeout (mặc định 10 giây) mà không replica nào xác nhận, source tự chuyển về async; khi replica đuổi kịp thì semi-sync bật lại.

Đo thật trên lab: một source và một replica MySQL 8.0, timeout đặt 2 giây, dùng docker pause để replica không trả lời được (hai lượt ngày 2026-10-20):

BướcKết quả
Replica bị pause, commit đầu tiênChờ 2.004–2.008 ms, đúng bằng timeout
Client khác đọc lúc 500 ms, khi commit kia còn chờ0 dòng: dữ liệu chưa hiện ra với ai
Sau commit đầu tiênSemi-sync chuyển OFF, các commit sau chỉ mất 2–6 ms, không có replica nào xác nhận (4 commit mỗi lượt)
Bỏ pause replicaSemi-sync tự bật lại sau vài ms

Vậy trong 2 giây đầu (10 giây nếu để mặc định), MySQL chọn consistency: thà bắt client chờ còn hơn báo commit xong mà chưa replica nào có. Hết timeout thì nó chọn availability: tiếp tục nhận ghi, chấp nhận nếu source sập lúc này thì những commit đó có thể mất khi failover. Hệ thống không thuộc hẳn ô CP hay AP, mà đổi lựa chọn theo một tham số cấu hình.

PACELC ​

Daniel Abadi bổ sung phần CAP bỏ qua: nếu có partition (P) thì đánh đổi availability với consistency (A và C); còn không (E, else), khi hệ thống chạy bình thường, thì đánh đổi latency với consistency (L và C). Lý do là muốn mọi bản sao nhất quán thì mỗi lệnh ghi phải chờ các bản sao khác xác nhận, và thời gian chờ đó tồn tại cả lúc mạng không có sự cố. Abadi xếp Dynamo, Cassandra vào nhóm PA/EL (bỏ consistency cả lúc bị chia cắt lẫn lúc bình thường), PNUTS của Yahoo vào PC/EL, còn các hệ ACID đầy đủ vào PC/EC.

Tài liệu MySQL nói rõ semi-sync làm mỗi commit chậm thêm ít nhất một round trip tới replica. Trong lab, source và replica chạy trên cùng một máy nên round trip chỉ khoảng 0,1 ms: p50 của async là 1,8–2,2 ms, của semi-sync là 1,7–2,0 ms, khác biệt chìm trong sai số (300 lần commit mỗi lượt, hai lượt). Khi replica nằm ở zone khác hay region khác, round trip là vài ms tới hàng trăm ms, và chi phí đó cộng thẳng vào mọi commit:

Ví dụ

Độ trễ commit tối thiểu theo mô hình: leader ghi disk rồi chờ đủ số replica xác nhận. Bỏ qua thời gian replica ghi relay log. Trong lab, leader và replica chạy cùng một máy: async đo được 1,8–2,2 ms, semi-sync 1,7–2,0 ms (p50), nên khác biệt chìm trong sai số.

Async2,0 ms
Leader sập có thể mất lệnh ghi đã báo OK mà replica chưa nhận. Replica sập không ảnh hưởng ghi.
Chờ đa số (leader + 1 replica, kiểu semi-sync hay Raft 3 node)3,0 ms
Không mất lệnh ghi đã báo OK khi một node sập. Một replica sập vẫn ghi được; cả hai sập thì không ghi được (hoặc semi-sync tự chuyển về async).
Chờ tất cả replica4,0 ms
Không mất lệnh ghi. Chỉ cần một replica chậm hay sập là mọi lệnh ghi bị chặn.

Tunable consistency ​

Một số hệ cho chọn mức nhất quán theo từng lệnh, thay vì cố định cho cả hệ thống:

  • Cassandra có các mức ONE, TWO, THREE, QUORUM, ALL, LOCAL_QUORUM, EACH_QUORUM, LOCAL_ONE, ANY cho từng lệnh đọc ghi. Theo tài liệu Cassandra, khi số replica được đọc cộng số replica được ghi lớn hơn số bản sao thì hai tập giao nhau và lần đọc thấy lệnh ghi. Nhưng như trang Replication đã giải thích, quorum vẫn không linearizable. Khi cần compare-and-set trên một partition, Cassandra có lightweight transaction.
  • DynamoDB đọc eventually consistent theo mặc định, và cho bật ConsistentRead để đọc strongly consistent trên bảng và local secondary index, với giá gấp đôi. Đọc từ global secondary index và stream thì luôn là eventually consistent. Global table nhiều region mặc định là eventually consistent giữa các region (thường trong khoảng một giây), và có chế độ multi-Region strong consistency, khi đó lệnh ghi phải replicate đồng bộ sang region khác trước khi trả về.
  • MySQL thì chọn qua cách đọc: đọc từ primary, đọc từ replica, hoặc đọc từ replica sau khi chờ GTID (Tách đọc / ghi); và chọn qua cách ghi: async hay semi-sync.

Các hệ thống thực tế đứng ở đâu ​

Hệ thốngLúc bình thườngLúc bị chia cắt
MySQL, PostgreSQL với async replicaĐọc primary gần như linearizable; đọc replica là eventual, thêm cách làm read your writes theo phiênPhía không có primary không ghi được; failover có thể mất lệnh ghi đã báo OK
MySQL semi-syncMỗi commit chờ một replica, chậm thêm ít nhất một round tripGiữ consistency tới hết timeout, rồi tự chuyển về async
etcd, ZooKeeper, ConsulGhi qua consensus; etcd mặc định đọc linearizable, ZooKeeper cần syncPhía thiểu số ngừng nhận ghi
Cassandra, ScyllaDBTunable; Abadi xếp Cassandra vào PA/ELVẫn nhận ghi nếu đủ số replica theo mức đã chọn
DynamoDBMặc định đọc eventual, có tuỳ chọn đọc strongTheo từng chế độ của bảng và global table
SpannerExternal consistency (strict serializable) dựa trên đồng hồ TrueTimeƯu tiên consistency; Brewer lập luận Spanner vẫn đúng với CAP dù có độ sẵn sàng rất cao

Chọn mức nhất quán theo nghiệp vụ ​

Không cần cả hệ thống cùng một mức. Mỗi loại dữ liệu chọn mức yếu nhất mà nghiệp vụ vẫn chấp nhận được, vì mức càng mạnh thì càng chậm và càng dễ ngừng phục vụ khi có sự cố:

Dữ liệuMức cầnCách làm hay gặp
Số dư, tồn kho, ghế đã đặt, usernameLinearizableĐọc ghi trên primary, ghi có điều kiện, hoặc consensus
Hồ sơ người dùng vừa sửaRead your writesĐọc primary vài giây sau khi ghi, hoặc chờ GTID
Bình luận và trả lờiCausalGhi các dữ liệu phụ thuộc vào cùng một partition
Lượt thích, lượt xem, feedEventualĐọc replica hay cache, chấp nhận lệch vài giây

Câu hỏi phỏng vấn hay gặp ​

“MongoDB (hay Cassandra) là CP hay AP?” Trả lời tốt là không xếp ô, mà nói cụ thể: mức nhất quán của lệnh đọc và lệnh ghi phụ thuộc cấu hình nào, và khi bị chia cắt thì phía nào vẫn phục vụ, phía nào trả lỗi, có mất lệnh ghi đã báo OK không.

Linearizable khác serializable thế nào? Serializable nói về transaction trên nhiều object, cần tương đương một thứ tự chạy lần lượt nào đó. Linearizable nói về một object, và thứ tự phải khớp thời gian thật. Hệ có cả hai là strict serializable.

CAP nói gì và không nói gì? CAP chỉ nói rằng khi mạng bị chia cắt thì không thể vừa linearizable vừa trả lời mọi request. Nó không nói gì về latency, không nói về các mô hình yếu hơn, và không cho phép “bỏ P”. PACELC bổ sung phần latency lúc mạng bình thường.

Ghi nhớ

  1. Consistency có nhiều nghĩa: chữ C trong ACID là ràng buộc dữ liệu, chữ C trong CAP là linearizable. Linearizable là trên một object và theo thời gian thật; serializable là trên transaction và không cần theo thời gian thật.
  2. Từ mạnh tới yếu: linearizable, sequential, causal, các đảm bảo theo phiên, eventual. Từ sequential trở lên thì không chạy được khi mạng bị chia cắt; causal và read your writes chạy được nếu client luôn nối vào cùng một node.
  3. CAP: khi mạng bị chia cắt, phía không liên lạc được hoặc ngừng trả lời, hoặc trả dữ liệu cũ. P không phải thứ được chọn, và phần lớn hệ thống không thuộc hẳn CP hay AP.
  4. PACELC: lúc mạng bình thường vẫn đổi latency lấy consistency, vì mỗi lệnh ghi nhất quán phải chờ các bản sao xác nhận.
  5. MySQL semi-sync giữ consistency tới hết timeout rồi tự chuyển về async, tức là đổi lựa chọn theo cấu hình.
  6. Chọn mức nhất quán theo từng loại dữ liệu, mức yếu nhất mà nghiệp vụ vẫn chấp nhận được.

Nguồn tham khảo ​

Đã đối chiếu (đọc nguồn ngày 2026-10-20):

NguồnDùng cho phần
Jepsen, Consistency Models, các trang Linearizable và CausalĐịnh nghĩa linearizable (nguyên tử, khớp thời gian thật, trên một object), sequential, causal; thứ bậc giữa các mô hình; nhóm không chạy được, sticky available, totally available khi mạng bị chia cắt
Seth Gilbert, Nancy Lynch, Perspectives on the CAP Theorem (IEEE Computer, 2012), bản PDFPhát biểu CAP với atomic (linearizable) read/write, availability, partition tolerance, phác thảo chứng minh với hai nhóm node
Martin Kleppmann, Please stop calling databases CP or AP (2015)Định nghĩa hẹp của C, A, P; single leader với async replication không phải CP cũng không phải AP; ZooKeeper không đọc linearizable mặc định; lời khuyên không xếp ô
Daniel Abadi, Problems with CAP, and Yahoo’s little known NoSQL system (2010)Định nghĩa PACELC; Dynamo và Cassandra là PA/EL, PNUTS là PC/EL, hệ ACID đầy đủ là PC/EC
MySQL 8.0 Reference Manual, Semisynchronous Replication và cấu hìnhSource chờ replica xác nhận relay log đã flush; hết timeout chuyển về async rồi tự bật lại; chậm thêm ít nhất một round trip; rpl_semi_sync_source_timeout mặc định 10.000 ms; AFTER_SYNC và AFTER_COMMIT
AWS docs, DynamoDB read consistencyMặc định eventual, ConsistentRead, đọc eventual rẻ bằng một nửa, GSI và stream luôn eventual, global table MREC và MRSC
Cassandra docs, DynamoDanh sách consistency level, quorum giao nhau, lightweight transaction cho compare-and-set trên một partition
Google Cloud, Spanner: TrueTime and external consistencyExternal consistency mạnh hơn linearizable và serializable, TrueTime

Từ kiến thức chung, chưa đối chiếu lần này:

  • Eric Brewer, CAP Twelve Years Later: How the “Rules” Have Changed (IEEE Computer, 2012): vì sao “2 trong 3” gây hiểu nhầm, lựa chọn chỉ xảy ra khi bị chia cắt. Không tải được bài trên InfoQ; các ý dùng trong trang đều có trong bài của Kleppmann và của Gilbert và Lynch.
  • Eric Brewer, Spanner, TrueTime and the CAP Theorem (2017): mới đọc phần tóm tắt, nói Spanner kết hợp độ sẵn sàng cao với strong consistency và vẫn đúng với CAP.
  • Maurice Herlihy, Jeannette Wing, Linearizability: A Correctness Condition for Concurrent Objects (1990), định nghĩa gốc của linearizable.
  • Lightweight transaction của Cassandra dùng Paxos.
  • Định nghĩa chính xác của bốn đảm bảo theo phiên (Terry và cộng sự, Session Guarantees for Weakly Consistent Replicated Data, 1994).

Code đã dùng để chạy thử ​

Chạy bằng npm run lab -- nhat-quan (cần npm run lab:up, khoảng 15 giây; script tạm docker pause replica rồi bỏ pause). Số trong trang lấy từ hai lượt ngày 2026-10-20 trên MacBook M2 với MySQL 8.0 trong Docker, xem Lab.

lab/nhat-quan/semisync.mjs: semi-sync lúc bình thường và lúc replica không trả lời
js
// Trang distributed/nhat-quan: MySQL semi-sync replication như một ví dụ CAP và PACELC thật. Cần lab (npm run up).
// 1. Lúc mạng bình thường (E trong PACELC): đo latency commit với async và với semi-sync.
// 2. Lúc replica không liên lạc được (P): `docker pause lab-replica`, rồi commit. Semi-sync chờ replica xác nhận
//    tới rpl_semi_sync_source_timeout (đặt 2 giây), trong lúc đó client khác chưa thấy dòng mới (AFTER_SYNC);
//    hết timeout thì MySQL tự chuyển về async để vẫn nhận ghi, tức là chọn availability.
// 3. Bỏ pause, xem semi-sync tự bật lại khi replica đuổi kịp.
import mysql from 'mysql2/promise'
import { execSync } from 'node:child_process'
import { MYSQL, REPLICA, CONTAINERS, sleep } from '../common/config.mjs'

const N = 300
const src = await mysql.createConnection(MYSQL)
const rep = await mysql.createConnection(REPLICA)
await src.query('CREATE DATABASE IF NOT EXISTS nhat_quan')
await src.query('DROP TABLE IF EXISTS nhat_quan.events')
await src.query('CREATE TABLE nhat_quan.events (id BIGINT AUTO_INCREMENT PRIMARY KEY, v VARCHAR(32), at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3))')

async function ensurePlugin(conn, name, so) {
  const [rows] = await conn.query('SELECT PLUGIN_NAME FROM information_schema.PLUGINS WHERE PLUGIN_NAME = ?', [name])
  if (!rows.length) await conn.query(`INSTALL PLUGIN ${name} SONAME '${so}'`)
}
await ensurePlugin(src, 'rpl_semi_sync_source', 'semisync_source.so')
await ensurePlugin(rep, 'rpl_semi_sync_replica', 'semisync_replica.so')
const status = async () => Object.fromEntries((await src.query("SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_source_%'"))[0].map((r) => [r.Variable_name.replace('Rpl_semi_sync_source_', ''), r.Value]))

async function timeInserts(label, n = N) {
  const lat = []
  for (let i = 0; i < n; i++) {
    const t0 = performance.now()
    await src.query('INSERT INTO nhat_quan.events (v) VALUES (?)', [label])
    lat.push(performance.now() - t0)
  }
  lat.sort((a, b) => a - b)
  const q = (p) => lat[Math.min(n - 1, Math.ceil(p * n) - 1)].toFixed(2).replace('.', ',')
  return `p50 ${q(0.5)} ms, p99 ${q(0.99)} ms`
}

console.log('\n=== 1. Mạng bình thường: latency commit (autocommit INSERT, ' + N + ' lần) ===')
await src.query('SET GLOBAL rpl_semi_sync_source_enabled = 0')
console.log('async:     ', await timeInserts('async'))

await src.query('SET GLOBAL rpl_semi_sync_source_timeout = 2000')
await src.query('SET GLOBAL rpl_semi_sync_source_enabled = 1')
await rep.query('SET GLOBAL rpl_semi_sync_replica_enabled = 1')
await rep.query('STOP REPLICA IO_THREAD')
await rep.query('START REPLICA IO_THREAD')
for (let i = 0; i < 50 && (await status()).clients !== '1'; i++) await sleep(100)
console.log('semi-sync: ', await timeInserts('semisync'), `| trạng thái: status ${(await status()).status}, clients ${(await status()).clients}`)

console.log('\n=== 2. Replica không liên lạc được (docker pause), rpl_semi_sync_source_timeout = 2000 ms ===')
execSync(`docker pause ${CONTAINERS.replica}`)
const other = await mysql.createConnection(MYSQL)
for (let i = 1; i <= 4; i++) {
  const t0 = performance.now()
  const p = src.query('INSERT INTO nhat_quan.events (v) VALUES (?)', ['paused-' + i])
  let seenWhileWaiting = null
  if (i === 1) {
    await sleep(500)
    const [[r]] = await other.query("SELECT COUNT(*) AS n FROM nhat_quan.events WHERE v = 'paused-1'")
    seenWhileWaiting = r.n
  }
  await p
  const ms = Math.round(performance.now() - t0)
  const s = await status()
  console.log(`commit ${i}: ${ms} ms, semi-sync status ${s.status}${seenWhileWaiting !== null ? `, client khác đọc lúc 500 ms thấy ${seenWhileWaiting} dòng` : ''}`)
}
const s2 = await status()
console.log(`commit được replica xác nhận: ${s2.yes_tx}, commit không được xác nhận: ${s2.no_tx}`)

console.log('\n=== 3. Bỏ pause replica ===')
execSync(`docker unpause ${CONTAINERS.replica}`)
const t1 = performance.now()
let back = null
for (let i = 0; i < 300; i++) {
  await src.query('INSERT INTO nhat_quan.events (v) VALUES (?)', ['after'])
  if ((await status()).status === 'ON') { back = Math.round(performance.now() - t1); break }
  await sleep(50)
}
console.log(back === null ? 'semi-sync chưa bật lại sau 15 giây' : `semi-sync tự bật lại sau ${back} ms (replica đuổi kịp)`)

await src.query('SET GLOBAL rpl_semi_sync_source_enabled = 0')
await rep.query('SET GLOBAL rpl_semi_sync_replica_enabled = 0')
await rep.query('STOP REPLICA IO_THREAD')
await rep.query('START REPLICA IO_THREAD')
await Promise.all([src.end(), rep.end(), other.end()])