Giao diện
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ảnh | Nghĩa | Ví dụ |
|---|---|---|
| Chữ C trong ACID | Dữ liệu luôn thoả các ràng buộc của ứng dụng | Tổ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 sao | Client đọ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à linearizable | Mụ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.
Đổ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ảo | Nghĩa |
|---|---|
| Read your writes | Client luôn đọc được những gì chính nó đã ghi |
| Monotonic reads | Client không bao giờ đọc lùi về giá trị cũ hơn giá trị nó đã đọc |
| Monotonic writes | Các lệnh ghi của một client được áp dụng đúng thứ tự client gửi |
| Writes follow reads | Lệ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óm | Mô hình | Khi mạng bị chia cắt |
|---|---|---|
| Không chạy được | Strict serializable, serializable, snapshot isolation, repeatable read, linearizable, sequential | Một số hoặc mọi node phải ngừng phục vụ |
| Sticky available | Causal, PRAM, read your writes | Vẫ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 available | Monotonic reads, monotonic writes, writes follow reads, read committed, read uncommitted | Node 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
synctrướ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
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ước | Kết quả |
|---|---|
| Replica bị pause, commit đầu tiên | Chờ 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ên | Semi-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 replica | Semi-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ố.
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,ANYcho 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ống | Lúc bình thường | Lú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ên | Phía không có primary không ghi được; failover có thể mất lệnh ghi đã báo OK |
| MySQL semi-sync | Mỗi commit chờ một replica, chậm thêm ít nhất một round trip | Giữ consistency tới hết timeout, rồi tự chuyển về async |
| etcd, ZooKeeper, Consul | Ghi qua consensus; etcd mặc định đọc linearizable, ZooKeeper cần sync | Phía thiểu số ngừng nhận ghi |
| Cassandra, ScyllaDB | Tunable; Abadi xếp Cassandra vào PA/EL | Vẫn nhận ghi nếu đủ số replica theo mức đã chọn |
| DynamoDB | Mặc định đọc eventual, có tuỳ chọn đọc strong | Theo từng chế độ của bảng và global table |
| Spanner | External 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ệu | Mức cần | Cách làm hay gặp |
|---|---|---|
| Số dư, tồn kho, ghế đã đặt, username | Linearizable | Đọc ghi trên primary, ghi có điều kiện, hoặc consensus |
| Hồ sơ người dùng vừa sửa | Read your writes | Đọc primary vài giây sau khi ghi, hoặc chờ GTID |
| Bình luận và trả lời | Causal | Ghi các dữ liệu phụ thuộc vào cùng một partition |
| Lượt thích, lượt xem, feed | Eventual | Đọ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ớ
- 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.
- 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.
- 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.
- 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.
- 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.
- 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ồn | Dù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 PDF | Phá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ình | Source 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 consistency | Mặ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, Dynamo | Danh sách consistency level, quorum giao nhau, lightweight transaction cho compare-and-set trên một partition |
| Google Cloud, Spanner: TrueTime and external consistency | External 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()])