Isolation & Snapshot: mỗi transaction nhìn thế giới ở thời điểm nào
Các bài trước để lại ba câu hỏi chưa trả lời. Một: bài CRUD & Query Model cảnh báo cursor không phải snapshot, một document có thể được trả về hai lần nếu nó bị cập nhật khi cursor đang mở. Hai: bài Query Execution Engine tái hiện được điều đó và hứa rằng một con số nhất quán (báo cáo, đối soát) phải được đọc trên một snapshot. Ba: bài Transactions & Atomicity cho thấy thay đổi chưa commit không ai thấy được, nhưng chưa nói transaction đọc thấy gì khi người khác commit giữa chừng.
Cả ba là cùng một câu hỏi: khi nhiều người đọc và ghi cùng lúc, ai nhìn thấy gì, và thấy ở thời điểm nào. Đó là isolation. Bài này trả lời bằng thí nghiệm: một transaction đứng yên trong khi dữ liệu đổi, một báo cáo cộng sai tiền mà không báo lỗi, hai bác sĩ cùng xin nghỉ để ca trực không còn ai, và cùng kịch bản đó trên PostgreSQL để so cho có số.
Bài này nằm ở đâu
- Cần biết trước: Transactions & Atomicity (session, vòng đời,
WriteConflict,TransientTransactionError, replica set tối thiểu), Query Execution Engine (yielding, đọc ngoài transaction không phải snapshot), CRUD & Query Model (cursor,getMore), Document Model (single-document atomicity, lost update) - Giới thiệu: isolation vs locking vs concurrency control, snapshot isolation trong transaction (snapshot được chụp lúc nào, thấy gì, không thấy gì), MVCC như một mô hình để hình dung, first-writer-wins, năm mức read concern (
local,available,majority,snapshot,linearizable) và cái giá đo được, đọc ngoài transaction (cursor trùng và sót), read concern"snapshot"ngoài transaction và giới hạn thời gian của nó, write skew và cách chặn, so với PostgreSQL (Read Committed, Repeatable Read, Serializable) - Dẫn tới: Durability & Consistency
Môi trường lab: MongoDB 8.3.11 trong Docker (image
mongo:8), replica set 3 node tênrs17: ba containermongo-rs17-a/-b/-ctrên một máy host Apple M4, mỗi container 1 CPU, 1 GB RAM, WiredTiger cache 0,25 GB, databaselab17. Client là mongosh trong các container ngắn hạn cùng network. PostgreSQL 18.6 (imagepostgres:18, containerpg17, 1 CPU, 1 GB RAM) chạy trên cùng network; script PostgreSQL viết bằng Python (psycopg3.2.13) và nối vào qua cổng publish. Ba node MongoDB chung một máy nên mạng giữa chúng gần như bằng không: mọi số đo có chờ majority là cận dưới. Số nào không đo thì ghi là minh hoạ.
Nhãn: [tài liệu] theo tài liệu chính thức; [quan sát] đo trong lab này (MongoDB 8.3.11, PostgreSQL 18.6); [suy luận] rút ra từ quan sát, không kiểm tra riêng; [chi tiết cài đặt] cách server đang làm, không phải cam kết API; [hình dung] mô hình để dễ nhớ.
Giải thích trong 30 giây
Isolation là lời hứa về những gì một thao tác được phép nhìn thấy từ các thao tác đang chạy song song với nó. Trong MongoDB, một transaction đọc trên một snapshot: một lát cắt của dữ liệu ở một thời điểm, đứng yên cho tới hết transaction, dù người khác commit bao nhiêu lần ở giữa. Ngoài transaction thì không có lát cắt nào cả: mỗi lần getMore, mỗi lần query nhường chỗ, thế giới có thể đã khác.
Snapshot không phải thuốc chữa mọi thứ. Nó chặn đọc trùng, đọc sót, con số lệch. Nó không chặn được write skew: hai transaction cùng đọc một snapshot, mỗi bên sửa một document riêng, cả hai commit, và luật nghiệp vụ vỡ. Muốn chặn cái đó phải thiết kế để hai transaction va vào nhau.
Hình dung trước: ảnh chụp cuốn sổ cái
Chị Hà đối soát doanh thu cuối ngày cho một cửa hàng có 2.000 ví khách. Sổ cái vẫn đang được ghi: các quầy liên tục chuyển tiền qua lại giữa các ví. Tổng số tiền trong 2.000 ví không bao giờ đổi, vì mỗi lần chuyển chỉ nhấc tiền từ ví này sang ví kia.
Cách 1: lật từng trang và cộng. Chị lật tới trang 900 thì quầy 3 chuyển 50.000đ từ ví ở trang 100 (chị đã cộng) sang ví ở trang 1.500 (chị chưa tới). Chị cộng thiếu 50.000đ. Lần khác, một ví bị đổi chỗ trong sổ khi chị đang lật, nên chị cộng nó hai lần hoặc bỏ sót. Tổng sai, và không ai báo lỗi.
Cách 2: chụp ảnh cả cuốn sổ lúc 17:00:00 rồi cộng trên ảnh. Quầy vẫn ghi sổ thật, nhưng chị chỉ nhìn ảnh. Tổng luôn đúng bằng con số của 17:00:00.
Cửa hàng MongoDB
────────────────────────────────────── ──────────────────────────────────────
sổ cái thật, đang được ghi liên tục collection, đang có người ghi
lật từng trang và cộng đọc ngoài transaction (cursor, getMore)
ảnh chụp sổ lúc 17:00:00 snapshot
chị Hà chỉ nhìn ảnh transaction đọc trên snapshot
ghi lên sổ thật khi trang đã bị sửa write conflict: bị từ chối, làm lại
sau 17:00 quầy thêm khách mới document mới không có trong snapshot[hình dung] Đây chỉ là cách hình dung. MongoDB không chép cả cuốn sổ: nó giữ nhiều phiên bản của từng document, mỗi phiên bản gắn một mốc thời gian, và "đọc trên snapshot" nghĩa là với mỗi document, chọn phiên bản mới nhất không vượt quá mốc. Ảnh cũng không chụp lúc chị cầm máy lên (
startTransaction) mà lúc chị mở trang đầu tiên (lệnh đầu tiên). Và chị chỉ đọc trên ảnh; ghi thì vẫn ghi vào sổ thật, nên nếu trang đã đổi sau 17:00 thì lệnh ghi bị từ chối chứ không ghi đè. Phiên bản cũ nằm ở đâu và bị dọn khi nào là chuyện của bài WiredTiger MVCC.
Isolation là gì, và nó không phải là gì
Trong ACID, chữ I hứa rằng các transaction chạy đồng thời cho kết quả như thể chúng chạy lần lượt. Hứa trọn vẹn lời đó (gọi là serializable) rất tốn, nên database cho chọn các mức yếu hơn, đổi lấy tốc độ và ít xung đột hơn. Mỗi mức yếu hơn cho phép một số bất thường (anomaly) lọt qua:
| Bất thường | Nghĩa là | Trong bài này |
|---|---|---|
| Đọc dữ liệu chưa an toàn (dirty read) | thấy thay đổi chưa commit, hoặc chưa tới majority | mục "Read concern" |
| Đọc không lặp lại | đọc hai lần cùng một dòng, ra hai giá trị | mục "Snapshot trong transaction" |
| Phantom | đọc hai lần cùng một điều kiện, tập kết quả đổi | mục "Snapshot trong transaction", "Cách 3" |
| Đọc không cùng một thời điểm | cursor trộn dữ liệu của nhiều thời điểm | mục "Đọc ngoài transaction" |
| Lost update | hai bên đọc rồi ghi đè lên nhau | mục "Snapshot trong transaction" |
| Write skew | hai bên đọc chung, ghi hai chỗ khác nhau, luật vỡ | mục "Write skew" |
Ba thuật ngữ hay bị trộn lẫn, nên tách chúng ra ngay từ đầu:
Isolation = LỜI HỨA với ứng dụng: tôi được thấy gì
Locking = MỘT cơ chế để giữ lời hứa (và để tránh hai bên ghi chồng nhau)
Concurrency control = TOÀN BỘ cơ chế cho phép nhiều thao tác chạy song song mà vẫn đúng
(phiên bản dữ liệu, lock, latch, phát hiện xung đột, retry)[tài liệu] MongoDB nói rõ lời hứa mặc định của nó: với đọc ngoài transaction, mức isolation là read uncommitted: tuỳ read concern, client có thể thấy kết quả một lệnh ghi trước khi nó bền vững, thậm chí thấy dữ liệu về sau bị rollback khi failover. Có hai ngoại lệ quan trọng. Một lệnh ghi lên một document là atomic: người đọc không bao giờ thấy document sửa dở một nửa số field. Và dữ liệu trong transaction chưa commit thì không ai thấy.
[quan sát] Lab của bài Transactions đã cho thấy khía cạnh locking (transaction giữ document đã sửa tới khi commit). Khía cạnh isolation của người đọc thì mới chỉ là lời hứa; bài này đo nó.
Setup và dataset
MongoDB version : 8.3.11 (mongo:8), replica set 3 node rs17, primary = mongo-rs17-a, FCV 8.3
Hardware : Apple M4 host; mỗi node 1 CPU, 1 GB RAM
Configuration : --wiredTigerCacheSizeGB 0.25; write concern mặc định w: "majority"
PostgreSQL : 18.6 (postgres:18), mặc định read committed
Dataset : lab17.items 10.000 document (đọc theo _id, đo latency read concern)
lab17.wallets 2.000 ví, balance 500–1.500, tổng 2.001.980, index { balance: 1 }
lab17.acct, oncall, roster, bookings, bookings_u: vài document cho từng kịch bảnSnapshot trong transaction
Một transaction đứng yên
Kịch bản: client A mở transaction với read concern "snapshot", đọc balance của ví t042:u001 và đếm document có k: 1. Client B (một connection khác, ngoài transaction) trừ 100.000đ khỏi ví đó và insert thêm một document k: 1. Cả hai commit xong, A đọc lại.
const s = db.getMongo().startSession();
s.startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } });
const sd = s.getDatabase("lab17");
sd.acct.findOne({ _id: "t042:u001" }).balance; // 1000000
sd.acct.countDocuments({ k: 1 }); // 2
// ... B: updateOne { $inc: { balance: -100000 } } + insertOne({ _id: "x3", k: 1 }), cả hai đã commit
sd.acct.findOne({ _id: "t042:u001" }).balance; // ?
sd.acct.countDocuments({ k: 1 }); // ?[quan sát]
[snapshot] A đọc lần 1: 1000000, đếm k=1: 2 | B ghi -100000 và insert x3 (đã commit) | B thấy: 900000
[snapshot] A đọc lần 2 (cùng transaction): 1000000, đếm k=1: 2 | sau commit A đọc ngoài txn: 900000Trong transaction, A thấy y nguyên bức tranh lúc nó bắt đầu: số dư cũ và số document cũ, dù B đã commit cả một thay đổi lẫn một document mới. Ra khỏi transaction thì A thấy 900.000 như mọi người. Đây là snapshot isolation: không có đọc không lặp lại (cùng một dòng, hai giá trị) và không có phantom (cùng một điều kiện, thêm dòng mới).
Read concern local và majority trong transaction
Nếu transaction mở bằng read concern "local" hay "majority" thì sao? [tài liệu] Trang Transactions ghi rằng ngay cả với "local", "bạn có thể quan sát isolation mạnh hơn", đọc từ một snapshot lúc transaction được mở; chữ "có thể" không phải cam kết, và trên sharded cluster "local"/"majority" không đảm bảo cùng snapshot giữa các shard. [quan sát] Cùng kịch bản, trên replica set 3 node, cả "local" lẫn "majority" đều cho y hệt "snapshot": đọc lại ra 1.000.000 và 2 document. Nhưng đó là cái lab thấy, còn cái tài liệu hứa chỉ có "snapshot". Cần snapshot thì đòi nó tường minh; ở mục "Read concern", lab không thấy "snapshot" đắt hơn "local".
Snapshot được chụp lúc nào
[quan sát] Mở transaction, rồi để B ghi, rồi mới đọc lệnh đầu tiên:
startTransaction -> B ghi -> lệnh đọc đầu tiên của A thấy: 900000 (B đã ghi: 900000)Snapshot không được chụp ở startTransaction, mà ở lệnh đầu tiên trong transaction. [chi tiết cài đặt] Điều này khớp với cách driver làm: startTransaction không gửi gì lên server; lệnh đầu tiên mới mang cờ bắt đầu transaction. Đừng mở transaction rồi mới đi làm việc khác, hy vọng nó "giữ" dữ liệu từ lúc mở.
Đọc xong rồi ghi: first writer wins
Snapshot đứng yên, còn dữ liệu thật thì không. Chuyện gì xảy ra nếu A dựa vào giá trị đã đọc để ghi, trong khi B đã sửa document đó?
A đọc balance = 1000000 (snapshot)
B updateOne ngoài transaction: balance 1000000 → 900000 (commit)
A updateOne { $set: { balance: 1000000 + 50000 } }
→ lỗi sau 1 ms: WriteConflict code 112 labels ["TransientTransactionError"]
balance cuối: 900000[tài liệu] Trang Production Considerations: nếu một lệnh ghi ngoài transaction sửa một document mà transaction sau đó định sửa, transaction bị abort vì write conflict. [quan sát] Người ghi trước thắng (first writer wins), người đến sau bị từ chối ngay (1 ms) và được gắn error label TransientTransactionError (một chuỗi server gắn kèm lỗi, báo rằng nên chạy lại) để làm lại từ đầu, lần này trên số dư mới. Chính cơ chế này chặn lost update trong transaction.
Cùng kịch bản ngoài transaction, ứng dụng đọc rồi ghi giá trị tính từ bản đọc:
ngoài txn: A đọc 1000000, B đọc 1000000, A ghi 900000, B ghi 1050000
-> balance cuối 1050000 (đúng ra 950000)Lệnh ghi của A bị mất mà không ai biết. Đây là lost update của bài Document Model; sửa bằng $inc (lệnh ghi biểu diễn cả "đọc và tính" ở phía server) hoặc bằng transaction. Transaction không "chờ" như PostgreSQL Read Committed (mục so sánh bên dưới sẽ đo): nó huỷ ngay. Làm lại ra sao, và nên giảm tranh chấp thế nào, là việc của bài Write Conflicts, Retry & Idempotency.
MVCC như một mô hình để hình dung
Làm sao hai người cùng đọc cùng một document mà thấy hai giá trị khác nhau? Mô hình để nhớ:
document w0007 mốc thời gian →
phiên bản 1: balance 316 ├──────────┤
phiên bản 2: balance 317 ├────────┤
phiên bản 3: balance 336 ├───────────→ (mới nhất)
▲
snapshot của A = mốc này → A thấy phiên bản 2
người đọc mới → thấy phiên bản 3[hình dung] Ghi một document không đè lên bản cũ; nó thêm một phiên bản mới. Mỗi người đọc có một mốc và luôn lấy phiên bản mới nhất không vượt mốc. Cách này gọi là MVCC (multi-version concurrency control). Hệ quả đo được:
- Người đọc không chặn người ghi, người ghi không chặn người đọc. [quan sát] A giữ transaction snapshot mở và đã đọc ví
w0007; B ghi cùng ví 20 lần trong lúc đó: p50 1,00 ms, max 2,19 ms, không lần nào phải chờ A (cả hai cùng thấy giá trị riêng của mình: A vẫn thấy 316, B thấy 336). - Phiên bản cũ phải được giữ chừng nào còn snapshot cần nó. Transaction mở càng lâu thì càng nhiều phiên bản cũ không dọn được, cache càng chịu áp lực. Đó là một lý do của giới hạn 60 giây cho transaction (bài Transactions); read concern
"snapshot"ngoài transaction có giới hạn riêng (mục "Cách sửa").
Phiên bản nằm ở đâu, update chain và history store hoạt động ra sao, thì bài WiredTiger MVCC giải thích. Ở đây ta chỉ cần mô hình: mốc thời gian + nhiều phiên bản.
Read concern: mỗi mức hứa gì
Read concern là tham số của lệnh đọc, trả lời câu hỏi "tôi muốn thấy dữ liệu đã an toàn đến mức nào". Nó khác isolation ở chỗ nó nói về độ bền của dữ liệu đọc được, và chỉ riêng "snapshot" mới cho thêm lời hứa về một thời điểm duy nhất.
| Mức | [tài liệu] hứa gì | Có thể rollback? | Trong transaction |
|---|---|---|---|
"local" | dữ liệu mới nhất trên node, không đảm bảo đã tới majority. Mặc định khi đọc trên primary (và secondary) | có | được |
"available" | như local về độ bền; trên sharded cluster có thể trả cả orphaned document, đổi lại độ trễ thấp nhất | có | không |
"majority" | dữ liệu đã được đa số node xác nhận, không bị rollback. Có hiệu lực đầy đủ trong transaction chỉ khi commit bằng w: "majority" | không | được |
"snapshot" | dữ liệu majority-committed tại một thời điểm duy nhất trong quá khứ gần; trong transaction chỉ có hiệu lực khi commit bằng w: "majority" | không | được; ngoài transaction chỉ cho find, aggregate và distinct (distinct chỉ trên collection không sharded) |
"linearizable" | phản ánh mọi lệnh ghi majority đã hoàn tất trước khi đọc bắt đầu; chỉ trên primary, nên dùng filter chọn đúng một document và maxTimeMS | không | không |
Hai điều bảng không nói thẳng. [tài liệu] Trong transaction, read concern đặt ở cấp transaction; read concern cấp collection hay database bị bỏ qua, và không nên đặt cho từng lệnh. Và chỉ "snapshot" nói tới một thời điểm: "majority" nói "dữ liệu này an toàn", không nói "dữ liệu này cùng một lúc". Mục "Đọc ngoài transaction" đo điều đó.
Đo cái giá
Một client tuần tự đọc ngẫu nhiên một document theo _id trong 10.000 document, trên primary, hệ thống không có ai ghi. 100 lần làm nóng rồi 1.000 lần đo mỗi ô, 7 lượt, bảng ghi median của các lượt:
| Cách đọc | p50 | p95 |
|---|---|---|
find + read concern "local" | 0,137 ms | 0,189 ms |
find + read concern "available" | 0,131 ms | 0,164 ms |
find + read concern "majority" | 0,131 ms | 0,169 ms |
find + read concern "snapshot" | 0,134 ms | 0,154 ms |
find + read concern "linearizable" | 0,555 ms | 1,000 ms |
transaction snapshot chỉ đọc (start, find, commit) | 0,660 ms | 0,925 ms |
[quan sát] local, available, majority, snapshot không phân biệt được ở quy mô này: 0,13–0,14 ms, chênh nhau nhỏ hơn dao động giữa các lượt (p50 từng lượt của local dao động 0,13–0,18). Tài liệu cũng nói "majority" có hiệu năng tương đương các mức khác. linearizable đắt hơn khoảng bốn lần. Một transaction chỉ-đọc đắt hơn khoảng năm lần, nhưng không phải vì snapshot: một lần đọc "snapshot" ngoài transaction giá bằng local. [suy luận] Phần chênh là lệnh commitTransaction (thêm một lượt đi về server) và việc mở, đóng session trong vòng đo (script tạo session mới cho mỗi lần); lab không tách từng phần.
[suy luận] linearizable chậm hơn có thể vì nó phải xác nhận với đa số node trước khi trả lời; lab không đo từng bước. Hệ thống ở đây không có người ghi và mạng gần bằng không, nên con số chỉ nói rằng snapshot không phải lý do để né read concern. Giá thật của snapshot nằm ở thời gian giữ nó (mục "Cách sửa"), không ở latency một lần đọc.
Mức nào thấy gì: kéo hai node secondary xuống
Để thấy tận mắt local khác majority, ta làm cho "đa số" không xác nhận được. Ghi stock = 100 với w: "majority" (xong, cả ba node có), rồi docker pause cả hai secondary, rồi ghi stock = 90 với w: 1 (primary tự áp dụng), rồi đọc lại bằng từng mức (kết nối thẳng vào primary, chỉ vài giây để primary chưa kịp step down):
ghi stock = 90 với w:1, ack sau 1 ms, modified 1
local stock = 90 (1 ms)
available stock = 90 (1 ms)
majority stock = 100 (0 ms)
snapshot stock = 100 (0 ms)
linearizable lỗi MaxTimeMSExpired code 50 sau 2011 ms (maxTimeMS đặt 2000)[quan sát] local thấy giá trị mới (đúng là chưa an toàn: nếu primary sập ngay lúc này, 90 có thể bị rollback). majority và snapshot vẫn thấy 100, giá trị cuối cùng mà đa số xác nhận. linearizable không trả lời: nó chờ đa số mà đa số không có mặt, và chỉ maxTimeMS cứu nó khỏi chờ vô hạn (đúng lời khuyên của tài liệu). Sau khi docker unpause hai secondary, đọc majority thấy 90.
Nghĩa là majority có thể cũ hơn những gì primary đã áp dụng. Đọc thấy lệnh ghi của chính mình qua secondary hay qua failover là chuyện causal consistency và write concern, trong bài Durability & Consistency.
Đọc ngoài transaction: cursor không đứng yên
Báo cáo cộng sai mà không báo lỗi
Bài Query Execution Engine tái hiện cursor trả trùng bằng hai lệnh sửa cố tình. Lần này làm cho nó giống production hơn: một người ghi chạy liên tục và một báo cáo đọc cả collection.
- Collection
wallets: 2.000 ví, tổng tiền 2.001.980, index{ balance: 1 }. - Người ghi: một tiến trình mongosh liên tục chuyển tiền giữa hai ví ngẫu nhiên, mỗi lần là một transaction (hai
$inc, commit vớiw: 1), nên tổng luôn bằng 2.001.980 ở mọi snapshot. Mỗi lần chạy 60 giây, làm được 1.614–1.896 lần chuyển mỗi giây. - Người đọc (báo cáo):
find().sort({ balance: 1 }).batchSize(100), cộngbalance, đếm số_idtrả về và số_idkhác nhau. 30 lượt mỗi cách đọc. Giữa hai batch báo cáo nghỉ 5 ms (giả lập app xử lý batch, tham số của lab); riêng "A0" không nghỉ.
Các cách đọc:
// A, A0 : find().sort({ balance: 1 }).batchSize(100) (local, mặc định)
// B : find().sort({ _id: 1 }).batchSize(100) (sort theo field không đổi)
// M : find().sort({ balance: 1 }).readConcern("majority")
// C : find().sort({ balance: 1 }).readConcern("snapshot") (ngoài transaction)
// D : cùng find trong transaction, readConcern snapshotĐo ba đợt, mỗi đợt người đọc làm 30 lượt cho mỗi cách trong lúc người ghi đang chạy (đợt 1 trong lần chạy thứ nhất của người ghi, đợt 2 và 3 trong lần chạy thứ hai). M được đo riêng, trong lần chạy thứ ba, cạnh một lượt A để đối chiếu. Mỗi ô ghi ba số theo ba đợt:
| Cách đọc | Lượt có document trùng | Lượt có document sót | Lượt có tổng sai | Số document trùng / sót (cộng 30 lượt) | Lệch tổng tối đa |
|---|---|---|---|---|---|
A local, sort balance, nghỉ 5 ms | 30, 30, 27 / 30 | 30, 30, 29 / 30 | 30 / 30 ở cả ba | 180 / 208, 132 / 114, 138 / 134 | 6.466, 6.279, 8.415 |
A0 local, sort balance, không nghỉ | 15, 19, 16 / 30 | 17, 21, 15 / 30 | 30 / 30 ở cả ba | 21 / 28, 32 / 40, 27 / 25 | 3.935, 5.506, 3.127 |
B local, sort _id | 0 | 0 | 30 / 30 ở cả ba | 0 / 0 | 484, 820, 474 |
M majority, sort balance (một đợt) | 28 / 30 | 29 / 30 | 30 / 30 | 105 / 95 | 6.281 |
C snapshot, sort balance | 0 | 0 | 0 | 0 / 0 | 0 |
D transaction snapshot, sort balance | 0 | 0 | 0 | 0 / 0 | 0 |
Khi không có người ghi, cả A, A0, B, C, D đều cho 0 trùng, 0 sót, tổng đúng 30 lượt trên 30. Vấn đề chỉ xuất hiện khi có ghi song song.
Đọc bảng
- A và A0: trùng, sót, tổng sai. Ở A (nghỉ giữa các batch), gần như mọi lượt có document bị trả hai lần và document khác bị bỏ sót, tổng lệch hàng nghìn đồng. Ở A0 (đọc liền) con số nhỏ hơn nhưng vẫn khoảng một nửa số lượt có trùng hoặc sót, và mọi lượt đều sai tổng: app không nghỉ thì khe hở hẹp lại, nhưng giữa hai
getMorecursor vẫn buông snapshot. - B: sort theo
_idhết trùng và sót, nhưng tổng vẫn sai 30 / 30 lần._idkhông đổi nên document không "chạy" trong index, nhưng cursor vẫn thấy các ví ở thời điểm khác nhau: ví này trước lần chuyển, ví kia sau. Sort ổn định chỉ giải nửa vấn đề. - M:
majoritykhông phải snapshot. Số liệu gần y hệt A (lượt A đo cùng lúc: trùng 28/30, sót 29/30, tổng sai 30/30).majorityhứa dữ liệu an toàn, không hứa dữ liệu cùng một lúc. Nhầm lẫn này khá phổ biến: "tôi đã dùng majority rồi mà". - C và D: không lần nào sai, trong 90 lượt. Cả
findngoài transaction với"snapshot"lẫn transaction cho tổng đúng 2.001.980 mọi lượt, dù người ghi đang chuyển 1.600–1.900 lần mỗi giây. Thời gian trung bình mỗi lượt (ba đợt): A 135–166 ms, C 138–165 ms, D 161–177 ms, phần lớn là 20 lần nghỉ 5 ms của lab: snapshot không làm chậm đáng kể ở quy mô này, nhưng đây không phải benchmark.
[tài liệu] Khớp với trang Read Isolation, Consistency, and Recency: không có cô lập, một read bắt đầu ở t1 có thể thấy bản cập nhật commit ở t2, có thể bỏ sót document khớp bị cập nhật trong lúc đọc, và cursor có thể trả cùng một document nhiều lần nếu thao tác khác đổi field thuộc index mà query đang dùng. Trang đó cũng nói "dùng read isolation để cải thiện tính nhất quán", tức read concern "snapshot".
[chi tiết cài đặt] Cơ chế đã giải ở bài Query Execution Engine: giữa các getMore (và mỗi lần query yield) cursor buông snapshot, rồi đi tiếp từ vị trí cũ trên index hiện tại. Ví bị đổi balance có key mới trong index. Nếu ví đã đọc rồi mà key mới nằm phía trước vị trí cursor, nó bị gặp lại lần nữa (trùng); nếu ví chưa đọc mà key mới bị dời về vùng cursor đã đi qua, nó không bao giờ được gặp (sót).
Cách sửa
Cần một con số nhất quán (báo cáo, đối soát, export)?
│
├── đọc trên một snapshot → find / aggregate với readConcern "snapshot"
│ (hoặc transaction snapshot)
│ ✓ không trùng, không sót, tổng đúng ✗ chỉ sống được một khoảng thời gian
│
├── giảm khe hở → sort theo field không đổi (_id, createdAt)
│ ✓ hết trùng và sót ✗ vẫn đọc dữ liệu của nhiều thời điểm
│
└── chịu được sai số → job idempotent, đọc theo khoảng _id/createdAt đã "đóng"
(dữ liệu quá khứ không còn bị sửa, như đơn của hôm qua)Cái giá của snapshot. [tài liệu] Một snapshot read chỉ làm được trong thời gian minSnapshotHistoryWindowInSeconds (mặc định 300 giây, từ 5.0): đọc lâu hơn thì có thể bị dừng. Ngoài transaction, "snapshot" chỉ dùng được cho find, aggregate và distinct (distinct chỉ trên collection không sharded), và nhận thêm tham số atClusterTime để đọc ở một mốc cụ thể; mốc cũ hơn cửa sổ đó thì nhận lỗi SnapshotTooOld. Trong transaction thì còn giới hạn 60 giây của chính transaction.
[quan sát] Hạ minSnapshotHistoryWindowInSeconds xuống 5 giây (chỉ trong lab), ghi một lệnh w: "majority" cho mốc snapshot mới, rồi đọc cursor "snapshot" 2.000 ví theo batch 100 (giữa hai batch có một lệnh ghi nhỏ):
nghỉ 0,1 s / batch: đọc đủ 2000 document sau 2,3 s
nghỉ 8 s / batch : dừng ở document 200 sau 16,0 s: SnapshotTooOld code 239
| Read timestamp Timestamp(1791529060, 8) is older than the oldest available timestamp.[quan sát] Ở lần chạy đầu, khi chưa có lệnh ghi majority trước khi mở cursor, ngay cả cursor nghỉ 0,1 s cũng dừng ở batch đầu với SnapshotTooOld. [suy luận] Mốc của "snapshot" là điểm majority-committed gần nhất, không phải "bây giờ"; hệ thống không có lệnh ghi nào trong hơn 5 giây thì mốc đó đã nằm ngoài cửa sổ. Với cửa sổ mặc định 300 giây, chuyện này khó xảy ra.
Snapshot không miễn phí: ứng dụng phải đọc đủ nhanh, hoặc chia việc thành đoạn ngắn (mỗi đoạn một snapshot; giữa các đoạn dữ liệu có thể đổi). Báo cáo chạy 20 phút cần cách khác, như số liệu tính trước (bài Aggregation Pipeline có $merge cho rollup).
Anomaly còn lại: write skew
Hai bác sĩ cùng xin nghỉ
Một ca trực luôn phải có ít nhất một bác sĩ. Đang có An và Bình. Cả hai cùng xin nghỉ. Mỗi yêu cầu chạy trong một transaction:
// mỗi transaction (snapshot): chỉ cho nghỉ nếu còn người khác trực
const n = oncall.countDocuments({ ward: "w3", onCall: true });
if (n >= 2) oncall.updateOne({ _id: me }, { $set: { onCall: false } });Mỗi transaction đọc cùng một snapshot (cả hai thấy 2 người), rồi sửa document của riêng mình (An sửa an, Bình sửa binh). Hai document khác nhau thì không có write conflict. Cả hai commit.
[quan sát] Lịch xen kẽ: T1 và T2 cùng mở, cùng đọc, rồi lần lượt ghi và commit:
== 1. Write skew: hai transaction snapshot, mỗi bên sửa document RIÊNG ==
T1 (An): thấy 2 người đang trực
T2 (Bình): thấy 2 người đang trực
T1: an xin nghỉ, commit ok
T2: binh xin nghỉ, commit ok
kết quả: đang trực = [] (luật: ít nhất 1)Không ai làm gì sai: mỗi transaction đều đúng luật trên snapshot của mình, cái sai chỉ hiện ra khi ghép hai quyết định lại. Đó là write skew: hai transaction đọc chung một tập dữ liệu, ghi hai chỗ khác nhau, và kết quả không tương đương thứ tự chạy lần lượt nào (An chạy trước thì Bình thấy 1 người và bị từ chối).
T1 T2
│ đọc: 2 người trực │ đọc: 2 người trực
│ │
├── ghi an.onCall=false ├── ghi binh.onCall=false (hai document khác nhau)
▼ ▼
commit ✓ commit ✓ → 0 người trực ✗Snapshot isolation thuần không chặn điều này, ở database nào cũng vậy. Tài liệu transaction của MongoDB không có mức "serializable": read concern "snapshot" là mức mạnh nhất.
Đây là một lịch xen kẽ hợp lệ do script ép ra, không phải cuộc đua ngẫu nhiên; lab không đo tần suất của nó trong tải thật. Chỉ cần nó có thể xảy ra là đủ để phải thiết kế tránh.
Cách 1: buộc hai transaction va vào nhau
Nếu cả hai cùng ghi một document chung, MongoDB sẽ phát hiện xung đột: người ghi sau bị abort. Thêm một document "chốt" (guard) cho mỗi ca trực, và cho mọi transaction $inc nó trước khi đọc:
guard.updateOne({ _id: "w3" }, { $inc: { v: 1 } }); // ghi chung: buộc hai transaction va nhau
const n = oncall.countDocuments({ ward: "w3", onCall: true });
if (n >= 2) oncall.updateOne({ _id: me }, { $set: { onCall: false } });[quan sát]
== 2. Sửa bằng cách cùng ghi một document 'guard' ==
T1 (An): thấy 2 người đang trực
T2 (Bình): lỗi WriteConflict ["TransientTransactionError"]
T1: an xin nghỉ, commit ok
Bình làm lại cả transaction:
T3 (Bình, lần 2): thấy 1 người đang trực
T3: chỉ còn 1 người trực, từ chối cho nghỉ
kết quả: đang trực = ["binh"]Người đến sau bị WriteConflict, làm lại, và lần này thấy 1 người trực nên từ chối: luật được giữ. Đổi thứ tự (T2 đọc trước, T1 commit, rồi T2 mới ghi guard) cũng cho WriteConflict ở T2 và kết quả y hệt. [tài liệu] Mục "In-progress Transactions and Stale Reads" của trang Production Considerations đưa ra chính kỹ thuật này: dùng findOneAndUpdate sửa một field để transaction "lấy lock" trên document và bảo đảm bản đọc không cũ.
Giá: mọi transaction của ca trực đó bị tuần tự hoá qua một document nóng. Hợp khi tranh chấp thấp và luật quan trọng (bài Write Conflicts bàn việc giảm tranh chấp).
Cách 2: đưa luật vào một document
Luật "còn ít nhất một người" nằm gọn trong một document nếu danh sách người trực nằm chung một document. Lệnh ghi một document là atomic và có điều kiện trong filter, nên không cần transaction:
// roster: { _id: "w3", onCall: ["an", "binh"] }
roster.updateOne({ _id: "w3", "onCall.1": { $exists: true }, onCall: me },
{ $pull: { onCall: me } }) // chỉ khớp khi còn ít nhất 2 phần tử[quan sát] An xin nghỉ: modified 1 | Bình xin nghỉ: modified 0 | roster = ["binh"]. Người thứ hai không khớp filter và không sửa gì: luật nằm trong chính lệnh ghi. Giá: danh sách nằm chung một document, nên phải đủ nhỏ và đủ ít người sửa (bài Data Modeling về document phình vô hạn).
Cách 3: unique index cho luật "không được trùng"
Write skew khó thấy hơn khi chỗ cần ghi chưa tồn tại: kiểm tra "chưa có ai đặt" rồi mới insert (write skew qua phantom). Hai người đặt cùng phòng R1 lúc 09:00. Cả hai đọc snapshot, cả hai thấy 0 chỗ đặt, mỗi người insert một document mới. Hai document mới không đụng nhau, nên không có write conflict.
[quan sát] Hai transaction snapshot, collection bookings (không có ràng buộc) và bookings_u (có unique index { room: 1, slot: 1 }):
-- collection bookings (không có unique index)
X thấy 0 đặt chỗ, Y thấy 0 đặt chỗ
X: insert + commit ok
Y: insert + commit ok
kết quả: 2 đặt chỗ cho R1 09:00
-- collection bookings_u (unique index {room, slot})
X thấy 0 đặt chỗ, Y thấy 0 đặt chỗ
X: insert + commit ok
Y: lỗi WriteConflict code 112 ["TransientTransactionError"]
kết quả: 1 đặt chỗ cho R1 09:00
-- bookings_u, insert thường (ngoài transaction) khi đã có chỗ:
lỗi code 11000
-- bookings_u, Y làm lại cả transaction sau WriteConflict:
Y (lần 2) thấy 1 đặt chỗ -> từ chốiKhông có unique index thì hai đặt chỗ cùng tồn tại. Có unique index thì database giữ luật: người thứ hai bị chặn. [quan sát] Ở lịch xen kẽ này (X commit sau khi Y đã có snapshot), Y nhận WriteConflict chứ không phải DuplicateKey; làm lại thì Y thấy chỗ đã có và từ chối. Một insert ngoài transaction khi chỗ đã có thì báo DuplicateKey (code 11000). [suy luận] Key của X mới hơn snapshot của Y, nên lần chèn trùng bị xử lý như một xung đột ghi; nếu X đã commit trước snapshot của Y thì nhiều khả năng Y nhận DuplicateKey, lab không thử trường hợp đó. Ứng dụng nên xử lý được cả hai mã lỗi.
Bài học chung: ràng buộc nằm ở database thì không phụ thuộc vào ai đọc gì.
| Luật cần giữ | Cách chặn write skew | Giá |
|---|---|---|
| "không được trùng" (phòng, email, số đơn) | unique index | thêm chi phí index (bài Specialized Indexes) |
| "ít nhất / nhiều nhất N" trên một nhóm nhỏ | đưa nhóm vào một document, ghi có điều kiện | document chung phải nhỏ |
| luật trải nhiều document, không biểu diễn được | cùng ghi một document chốt (guard) trong transaction | tuần tự hoá qua một document nóng, phải retry |
So với PostgreSQL
Cùng các kịch bản chạy trên PostgreSQL 18.6 (mặc định read committed) bằng hai connection xen kẽ như lab MongoDB. Không quy đổi "mức này của MongoDB bằng mức kia của PostgreSQL": ta so hành vi đo được.
Write skew: ba mức isolation
READ COMMITTED thấy 2,2 | T1 commit ok | T2 commit ok | còn trực: []
REPEATABLE READ thấy 2,2 | T1 commit ok | T2 commit ok | còn trực: []
SERIALIZABLE thấy 2,2 | T1 commit ok
| T2 lỗi SerializationFailure SQLSTATE 40001:
could not serialize access due to read/write dependencies among transactions
| còn trực: ['binh'][quan sát] Write skew xảy ra ở Read Committed và cả Repeatable Read (snapshot isolation): không ai trực. Chỉ Serializable chặn nó: giao dịch thứ hai bị từ chối ở bước commit với SQLSTATE 40001, và ứng dụng làm lại. Đó là điều MongoDB không có: không có một mức isolation nào của transaction để "đòi" database tự phát hiện write skew. Ở MongoDB, chặn nó là việc của thiết kế (ba cách ở trên).
[tài liệu PostgreSQL] Serializable dùng predicate lock; loại lock này không chặn ai, chỉ dùng để phát hiện phụ thuộc giữa các giao dịch đồng thời. Giao dịch bị loại thì ứng dụng retry toàn bộ.
Hai transaction cùng sửa một dòng
Bài Transactions đã so cách hai bên xử lý xung đột ghi ở Read Committed; ở đây thêm Repeatable Read và trường hợp đọc rồi ghi. T1 sửa dòng và giữ 1 giây rồi commit. T2 (mở trước, đã đọc dòng đó) cũng sửa dòng ấy. delta là balance = balance + 50000 (tính ở phía server), rmw là đọc rồi ghi giá trị tính ở ứng dụng (như lost update):
READ COMMITTED delta T2 chờ 1006 ms -> ok | balance cuối 950000 (đúng)
READ COMMITTED rmw T2 chờ 1006 ms -> ok | balance cuối 1050000 (lost update)
REPEATABLE READ delta T2 chờ 1006 ms -> 40001 could not serialize access due to concurrent update
REPEATABLE READ rmw T2 chờ 1008 ms -> 40001 could not serialize access due to concurrent update
| balance cuối 900000 (chỉ có lệnh của T1)[quan sát] Ở cả hai mức, T2 đứng chờ đúng bằng thời gian T1 giữ dòng (khoảng 1 giây). Transaction MongoDB thì không chờ: gặp document mà transaction khác đang giữ, nó nhận WriteConflict ngay (thí nghiệm guard ở trên); gặp document đã bị sửa sau snapshot, nó bị huỷ sau 1 ms (mục "Đọc xong rồi ghi"). [tài liệu] Ngược lại, một lệnh ghi ngoài transaction gặp document đang bị transaction giữ thì chờ tới khi transaction kết thúc. Sau khi T1 commit:
- Read Committed: T2 chạy tiếp. Với
delta, PostgreSQL áp dụng lệnh lên phiên bản mới của dòng: kết quả đúng. Vớirmw, T2 ghi đè giá trị cũ mà ứng dụng tự tính: lost update, y hệt MongoDB ngoài transaction. - Repeatable Read: T2 chờ rồi bị
40001. Giống MongoDB ở chỗ người đến sau phải làm lại, khác ở chỗ PostgreSQL bắt chờ trước rồi mới báo, còn MongoDB báo ngay.
Phantom với unique constraint
REPEATABLE READ bookings thấy 0,0 | X: ok | Y: ok (2 ms) | 2 đặt chỗ
REPEATABLE READ bookings_u thấy 0,0 | X: ok | Y: 23505 UniqueViolation (1 ms) | 1 đặt chỗ
SERIALIZABLE bookings thấy 0,0 | X: ok | Y: 40001 SerializationFailure | 1 đặt chỗ
SERIALIZABLE bookings_u thấy 0,0 | X: ok | Y: 40001 SerializationFailure | 1 đặt chỗ[quan sát] Có unique constraint thì Y bị chặn (23505; MongoDB trong transaction báo WriteConflict). Không có thì Repeatable Read cho cả hai đặt chỗ và chỉ Serializable cứu được. Cùng bài học: ràng buộc ở database là hàng rào chắc nhất.
Bảng so sánh
| MongoDB 8.3 | PostgreSQL 18 | |
|---|---|---|
Một câu SELECT/find đơn lẻ (không transaction) có nhìn một thời điểm duy nhất? | không: cursor trộn nhiều thời điểm (lab: tổng sai 30/30 lượt) | có: mặc định Read Committed vẫn cho mỗi câu lệnh một snapshot riêng |
| Muốn đọc nhất quán | tự đòi: read concern "snapshot" hoặc transaction | mặc định đã có cho từng câu; nhiều câu cùng snapshot thì Repeatable Read trở lên |
| Snapshot được chụp lúc | lệnh đầu tiên của transaction (lab) | [tài liệu] Repeatable Read: lệnh đầu tiên không phải điều khiển transaction; Read Committed: đầu mỗi câu lệnh |
| Hai transaction cùng sửa một dòng | transaction đến sau bị huỷ ngay (WriteConflict, lab: 1 ms) | Read Committed: bên đến sau chờ, rồi chạy tiếp; Repeatable Read: chờ, rồi 40001 |
| Write skew | xảy ra trong transaction snapshot; phải tự thiết kế để chặn | xảy ra ở Read Committed và Repeatable Read; Serializable chặn (40001) |
| Có mức "serializable" không | không có trong tài liệu transaction | có (SSI, predicate lock) |
| Ràng buộc unique chặn phantom | có (lab: WriteConflict khi hai transaction đua nhau, DuplicateKey ngoài transaction) | có (23505) |
| Lỗi để retry | error label TransientTransactionError | SQLSTATE 40001, 40P01 |
[tài liệu PostgreSQL] Bảng mức isolation của PostgreSQL: Repeatable Read không cho phantom read (chặt hơn tiêu chuẩn SQL yêu cầu) nhưng vẫn cho serialization anomaly (write skew); Read Uncommitted hoạt động y hệt Read Committed.
Cách đọc: về hành vi quan sát, transaction snapshot của MongoDB gần Repeatable Read của PostgreSQL (cùng write skew, cùng first-writer-wins, cùng snapshot ổn định), nhưng không bằng nhau: khác ở cách xử lý xung đột (huỷ ngay thay vì chờ), ở việc không có Serializable, và ở chỗ ngoài transaction MongoDB không có snapshot theo từng lệnh như Read Committed. Tên mức không nói được gì về hành vi; phải đo hoặc đọc đúng tài liệu của đúng phiên bản.
Hai bên không "đúng/sai": PostgreSQL mặc định an toàn hơn cho một câu lệnh đơn lẻ và có Serializable, đổi lại chi phí theo dõi phụ thuộc. MongoDB mặc định rẻ cho thao tác một document, cho snapshot khi đòi, và giao việc chặn write skew cho thiết kế. So sánh tổng thể ở bài Anti-patterns & MongoDB vs PostgreSQL.
Những lỗi thường gặp
- Dùng cursor thường để tính con số cần chính xác. Lab: tổng sai 30/30 lượt khi có người ghi, ngay cả khi sort theo
_id. Dùngfindhoặcaggregatevới"snapshot", hoặc transaction. - Tin
majoritylà snapshot.majorityhứa dữ liệu đã an toàn, không hứa một thời điểm duy nhất (lab: 28/30 lượt trùng, 29/30 lượt sót). - Giữ snapshot quá lâu. Quá hạn: 60 giây trong transaction, 300 giây mặc định ngoài transaction.
- Đọc rồi ghi giá trị tính ở app, ngoài transaction. Lost update: lab mất 100.000đ mà không báo lỗi. Dùng
$inc/toán tử cập nhật, hoặc làm trong transaction. - Nghĩ transaction snapshot là serializable. Lab: hai bác sĩ cùng nghỉ, ca trực rỗng. Chặn bằng unique index, document chung hoặc
guard. - Đặt read concern cho từng lệnh trong transaction. Nó đặt ở cấp transaction; tài liệu khuyên không đặt riêng lẻ.
- Giả định hành vi PostgreSQL. Ở MongoDB transaction đến sau không chờ mà bị huỷ, một
findkhông tự có snapshot, và không có mức Serializable.
Tóm tắt
- Isolation là lời hứa về việc được thấy gì; locking chỉ là một cơ chế. Hình dung bằng MVCC: nhiều phiên bản mỗi document, mỗi snapshot một mốc thời gian.
- Transaction đọc trên snapshot (lab: số dư 1.000.000 và số document không đổi dù B commit giữa chừng). Snapshot được chụp ở lệnh đầu tiên. Người ghi sau trên document đã đổi bị
WriteConflictsau ~1 ms (first writer wins), chặn lost update. - Read concern:
local(có thể rollback),majority(an toàn, không hứa một thời điểm),snapshot(một thời điểm),linearizable(chỉ primary, nên cómaxTimeMS). Latencylocal/majority/snapshot~0,13 ms;linearizable~0,56 ms. - Ngoài transaction, với người ghi song song, cursor trùng và sót document ở 27–30/30 lượt (đọc có nghỉ giữa batch), tổng sai 30/30; sort theo
_idhết trùng nhưng tổng vẫn sai;majoritykhông giúp;snapshotvà transaction đúng 90/90 lượt. Giá: thời hạn (300 giây mặc định, lab:SnapshotTooOld). - Write skew vẫn xảy ra trong transaction snapshot (lab: 0 bác sĩ trực, 2 đặt chỗ trùng phòng). Chặn bằng unique index, một document chung, hoặc document chốt.
- PostgreSQL 18.6: Read Committed và Repeatable Read cũng bị write skew; chỉ Serializable chặn (
40001). Cùng sửa một dòng trong transaction: PostgreSQL chờ (lab ~1 giây), MongoDB huỷ ngay (lab: 1 ms). Mỗi câu lệnh PostgreSQL có snapshot riêng,findcủa MongoDB thì không.
Hỏi & đáp
Transaction A (read concern "snapshot") đọc balance = 1.000.000 và đếm được 2 document có k: 1. Sau đó client B commit balance = 900.000 và thêm một document k: 1. A đọc lại trong cùng transaction. A thấy gì?
Một báo cáo cộng balance của 2.000 ví qua cursor, trong khi một tiến trình khác chuyển tiền giữa các ví (tổng tiền không đổi). Cách nào làm báo cáo luôn ra đúng tổng?
Hai secondary tạm mất. Primary vừa nhận một lệnh ghi w: 1 đổi stock từ 100 xuống 90. Đọc stock bằng read concern "majority" trên primary trả gì?
Hệ thống đặt phòng: transaction (snapshot) kiểm tra "chưa có ai đặt R1 lúc 09:00" rồi insert. Hai người đặt cùng lúc và cả hai commit thành công, ra hai đặt chỗ. Cách sửa nào đúng nhất?
Hai giao dịch cùng sửa một bản ghi; giao dịch đầu chưa commit, giao dịch hai đến sau. Lab đo ở PostgreSQL (Read Committed) và MongoDB (transaction). Điều nào đúng?
Hai transaction cùng đọc một snapshot (đang có 2 bác sĩ trực), mỗi bên cho một bác sĩ khác nhau nghỉ, cả hai commit. Điều gì đúng?
Hai trưởng ca cùng nhìn bản chụp bảng trực lúc 9:00 (đang có 2 người), mỗi người cho một người khác nhau nghỉ, và bảng thật còn 0 người. Vì sao, và làm gì để tránh?
Nếu bỏ hết thuật ngữ: khi nhiều người cùng làm việc trên một cuốn sổ đang được ghi, người đọc có hai lựa chọn. Lật từng trang thì thấy trang này lúc này, trang kia lúc khác, nên con số có thể sai mà không ai báo. Hoặc chụp ảnh cả cuốn sổ một lần rồi chỉ nhìn ảnh: con số luôn đúng với một thời điểm, nhưng ảnh có hạn dùng, và hai người cùng nhìn một ảnh có thể đưa ra hai quyết định mà ghép lại thì sai. Muốn chặn cái sau, đừng trông vào ảnh: phải buộc họ cùng ghi vào một chỗ, hoặc để luật nằm ngay trên sổ.
Bài tiếp theo
Bài này trả lời "ai thấy gì, ở thời điểm nào". Còn "dữ liệu đó an toàn đến đâu"? Lệnh ghi w: 1 ở trên nhận ack sau 1 ms, trước khi bất kỳ secondary nào có nó; nếu primary sập ngay sau đó thì sao? majority nghĩa là gì khi một node mất, và vì sao một lệnh đọc secondary có thể không thấy lệnh ghi của chính mình vừa làm?
Durability & Consistency trả lời những câu đó: write concern (w, j, wtimeout), read concern majority đi cùng write concern ra sao, causal consistency qua session, và cách retryable write/read hoạt động. Còn cơ chế bên dưới (journal, majority commit point lúc failover) là chuyện của bài Journal & Checkpoint và bài Elections, Failover & Majority.
Tài liệu tham khảo
- Read Isolation, Consistency, and Recency (read uncommitted, non-point-in-time reads, cursor snapshot, causal consistency)
- Read Concern (local, available, majority, snapshot, linearizable; transaction-level read concern)
- Read Concern "snapshot" (outside transactions, atClusterTime, minSnapshotHistoryWindowInSeconds)
- Read Concern "snapshot", bản 8.0
- Transactions (read concern in transactions, snapshot isolation on sharded clusters)
- Production Considerations (in-progress transactions and write conflicts, stale reads, findOneAndUpdate to take a lock)
- Server Parameters (minSnapshotHistoryWindowInSeconds)
- PostgreSQL: Transaction Isolation (Read Committed, Repeatable Read, Serializable, bảng bất thường theo mức)
- PostgreSQL: Serialization Failure Handling (40001, retry)