Giao dịch và những cái nhãn nói dốiLàm trọn gói, hoặc không làm gìTransactions and the labels that lie
Giao dịch là công cụ được dựng lên để giấu đi một mớ hỗn độn: phần cứng chết giữa chừng, ứng dụng sập sau khi mới ghi được nửa dữ liệu, mạng đứt, nhiều client cùng ghi đè lên nhau. Nó gom vài thao tác đọc/ghi thành một đơn vị logic duy nhất — hoặc thành công trọn vẹn (commit), hoặc thất bại trọn vẹn (abort, rollback). Nhưng Kleppmann cảnh báo ngay từ đầu chương: giao dịch không phải quy luật tự nhiên, mà là một lựa chọn kỹ thuật có giá, và cái nhãn ACID dán lên nó nói được ít hơn ta tưởng rất nhiều12.
Giao dịch giống như mua hàng: hoặc bạn trả tiền và nhận hàng, hoặc không có chuyện gì xảy ra cả — chứ không có kiểu trả tiền rồi mất hàng. Máy tính cũng vậy: chuyển tiền phải trừ bên này và cộng bên kia, không được làm nửa vời. Cái khó là: nhiều người cùng bấm một lúc, và cái nhãn "an toàn" dán trên cơ sở dữ liệu không phải lúc nào cũng nói thật.
A transaction is a device built to hide a mess: hardware dying mid-write, an application crashing after writing only half its data, networks cutting out, several clients overwriting each other. It groups a few reads and writes into one logical unit — either it all succeeds (commit) or it all fails (abort, rollback). But Kleppmann warns from the opening pages: transactions are not a law of nature, they are an engineering choice with a price, and the ACID label stuck on them says far less than we assume12.
Đây là chương về một bộ từ vựng bị lạm dụng. ACID nghe như một bảo đảm chặt chẽ, nhưng ba chữ Atomicity, Isolation, Durability mới có nghĩa tương đối rõ; còn Consistency là chữ thuộc về ứng dụng chứ không phải cơ sở dữ liệu1. Tệ hơn: hầu hết hệ thực tế không chạy ở mức cô lập tuần tự mà ở các mức cô lập yếu, và tên gọi của những mức ấy khác nhau giữa các sản phẩm — cái PostgreSQL gọi “repeatable read” chính là snapshot isolation9, còn cái Oracle gọi “serializable” cũng là snapshot isolation5. Hiểu chương này nghĩa là bỏ cái nhãn xuống và hỏi một câu duy nhất: chính xác thì hệ này chặn được race condition nào?
Người ta hay khoe cơ sở dữ liệu của mình "an toàn tuyệt đối". Nhưng mỗi hãng hiểu chữ "an toàn" một kiểu, thậm chí dùng cùng một cái tên để chỉ hai thứ khác nhau. Nhớ nhé: đừng tin cái nhãn, hãy hỏi xem nó chặn được đúng những lỗi nào khi nhiều người cùng làm một lúc.
This is a chapter about an abused vocabulary. ACID sounds like a tight guarantee, but only Atomicity, Isolation and Durability have reasonably clear meanings; Consistency belongs to the application, not to the database1. Worse: most real systems do not run at serializable isolation but at weak isolation levels, and the names of those levels differ between products — what PostgreSQL calls “repeatable read” is snapshot isolation9, and what Oracle calls “serializable” is also snapshot isolation5. Understanding this chapter means putting the label down and asking one question: exactly which race conditions does this system prevent?
HỨA GÌ?WHAT DOES A
TRANSACTION
PROMISE?
Bốn chữ, và một chữ đi lạcFour letters, and one that wandered off
Kleppmann mở chương bằng việc tháo tung từ viết tắt nổi tiếng nhất ngành. ACID được Theo Härder và Andreas Reuter đặt ra năm 1983 để có một thuật ngữ chính xác cho khả năng chịu lỗi của hệ cơ sở dữ liệu7. Nhưng mức độ hiện thực của nó khác nhau rất xa giữa các sản phẩm, đến mức ông kết luận rằng ngày nay ACID phần lớn đã thành một từ khóa tiếp thị: nghe "hệ này có ACID" gần như không cho bạn biết điều gì cụ thể1. Chính vì thế ông tự viết một bộ kiểm thử — Hermitage — để đo xem mỗi hệ thật sự làm gì56.
Bạn sẽ hay nghe người ta khoe "cơ sở dữ liệu của tôi có ACID". Bốn chữ đó nghe rất oách. Nhưng mỗi hãng lại làm một kiểu, nên nghe xong bạn vẫn chưa biết nó thật sự bảo vệ bạn khỏi cái gì. Nhớ nhé: nghe cái nhãn xong thì phải hỏi tiếp một câu — "cụ thể là chặn được lỗi nào?".
Kleppmann opens by taking apart the industry's most famous acronym. ACID was coined by Theo Härder and Andreas Reuter in 1983 to give a precise term for a database system's fault tolerance7. But how far it is actually implemented varies so widely between products that he concludes ACID has largely become a marketing term: hearing "this system is ACID" tells you almost nothing concrete1. Which is why he wrote a test suite of his own — Hermitage — to measure what each system really does56.
Bốn chữ không cùng hạng với nhau. Atomicity là "hủy được": hỏng giữa chừng thì mọi ghi bị vứt bỏ. Isolation là "cứ coi như mình chạy một mình". Durability là "đã commit thì không mất". Còn Consistency — tính đúng đắn của dữ liệu — là thứ ứng dụng định nghĩa; cơ sở dữ liệu chỉ ép giúp được một mảnh rất hẹp1.
Bốn chữ nghe như một bộ, nhưng ba chữ là việc của máy, còn một chữ là việc của người viết ứng dụng. Máy lo: hỏng thì hủy, không giẫm chân nhau, ghi rồi thì còn. Người lo: thế nào mới gọi là dữ liệu "đúng". Nhớ nhé: máy không biết tài khoản của bạn không được âm — bạn phải nói cho nó.
The four letters are not of the same rank. Atomicity means abortable: if it fails midway, all its writes are thrown away. Isolation means "pretend you are running alone". Durability means "once committed, it stays". Consistency — the correctness of the data — is defined by the application; the database can only enforce a very narrow slice of it1.
Kleppmann nhận xét thẳng rằng chữ C gần như được "nhét vào" cho từ viết tắt đọc xuôi tai, chứ không cùng loại với ba chữ kia1. Đối trọng thường được nêu — BASE (Basically Available, Soft state, Eventual consistency) — theo ông còn mơ hồ hơn nữa: định nghĩa rõ ràng duy nhất của nó là "không phải ACID". Bài học thực dụng: khi ai đó nói hệ của họ "có ACID", câu hỏi cần đặt không phải là đúng hay sai, mà là mức cô lập mặc định là gì và nó đã được kiểm chứng ra sao — đúng tinh thần bộ kiểm thử Hermitage5.
-
Atomicity · not concurrency, but abortability
Nguyên tử nghĩa là "hủy được"Atomic really means abortable
Chữ atomic trong ACID không cùng nghĩa với atomic trong lập trình đồng thời (nơi nó nói về việc luồng khác không thấy được trạng thái giữa chừng). Trong giao dịch, nó nói về cái gì xảy ra khi có lỗi: nếu client mất kết nối, đĩa hết chỗ, hay ràng buộc bị vi phạm sau khi đã ghi vài dòng, thì mọi ghi của giao dịch bị vứt bỏ như chưa từng có. Kleppmann nói abortability mới là cái tên mô tả đúng1.
"Nguyên tử" nghe như vật lý, nhưng ý rất đơn giản: không có nửa vời. Giống như viết thư — hoặc bạn gửi cả bức, hoặc bạn vò nó đi và không ai đọc được dòng nào. Nhớ nhé: làm hỏng giữa chừng thì xóa sạch, chứ đừng để lại một nửa.
The word atomic in ACID does not mean what atomic means in concurrent programming (where it is about other threads not seeing an intermediate state). In transactions it is about what happens when something goes wrong: if the client loses its connection, the disk fills up, or a constraint is violated after a few rows were written, every write of that transaction is thrown away as if it never happened. Kleppmann says abortability would be the more accurate name1.
Vì sao quan trọngCó abort thì ứng dụng mới dám thử lại an toàn: thất bại không để lại dấu vết nửa vời cần đi dọn bằng tay.Ý chínhCó nút "hủy" đáng tin thì mới dám làm lại từ đầu mà không sợ dữ liệu lộn xộn.Why it mattersAbort is what makes retrying safe: a failure leaves no half-finished mess to clean up by hand.Cơ chế
Thử lại một giao dịch bị abort là cách xử lỗi đơn giản nhưng không hoàn hảo: nếu mạng đứt sau khi server đã commit mà trước khi client nhận xác nhận, thử lại sẽ làm hai lần. Kleppmann nêu rõ giới hạn này — cần cơ chế khử trùng lặp ở tầng ứng dụng, và tránh thử lại vô hạn khi nguyên nhân là quá tải.
Ranh giới hay bị trộn
Trong lập trình đa luồng, "atomic" nói về khả năng quan sát trạng thái giữa chừng — đó chính là việc của isolation trong ACID, không phải của atomicity. Trộn hai nghĩa này là nguồn hiểu lầm kinh điển khi đọc tài liệu CSDL.
-
Consistency · the application's word, not the database's
Chữ C không thuộc về cơ sở dữ liệuThe C does not belong to the database
Đây là điểm sắc nhất của phần đầu chương. "Nhất quán" ở đây nghĩa là dữ liệu thỏa những bất biến mà ứng dụng coi là đúng — ví dụ tổng tiền hai tài khoản sau khi chuyển khoản phải không đổi. Cơ sở dữ liệu không biết bất biến ấy. Nó chỉ giúp được một mảnh hẹp: ràng buộc unique, khóa ngoại, check. Phần còn lại là trách nhiệm của ứng dụng — và vì thế Kleppmann nói chữ C không thật sự thuộc về ACID1. Tài liệu PostgreSQL cũng nói đúng điều đó theo cách của nó: ở mức repeatable read, "các nỗ lực ép quy tắc nghiệp vụ bằng giao dịch nhiều khả năng sẽ không chạy đúng nếu không dùng khóa tường minh một cách cẩn thận"9.
Máy tính không biết "đúng" nghĩa là gì trong câu chuyện của bạn. Nó không biết tài khoản không được âm tiền, hay một lớp học không được quá 30 bạn. Bạn phải viết luật đó ra, còn máy chỉ giữ vài luật đơn giản như "không được trùng tên". Nhớ nhé: luật của bài toán là do bạn đặt, máy chỉ giúp canh vài luật nhỏ.
This is the sharpest point in the first half of the chapter. "Consistency" here means the data satisfies invariants the application considers true — for example that the total across two accounts is unchanged after a transfer. The database does not know that invariant. It only helps with a narrow slice: unique constraints, foreign keys, checks. The rest is the application's job — which is why Kleppmann says the C does not really belong in ACID1. PostgreSQL's manual says the same thing in its own words: at repeatable read, "attempts to enforce business rules by transactions … are not likely to work correctly without careful use of explicit locks"9.
Hệ quả thực tếĐừng kỳ vọng chọn đúng mức cô lập là dữ liệu tự "đúng". Cô lập chỉ chặn race condition; tính đúng đắn vẫn phải do bạn phát biểu và ép.Ý chínhChọn chế độ an toàn của máy không thay được việc bạn phải nói rõ "thế nào là đúng".The practical consequenceDon't expect the right isolation level to make data "correct" by itself. Isolation only blocks race conditions; correctness is still yours to state and enforce.Một chữ, bốn nghĩa
Kleppmann liệt kê mấy nghĩa khác hẳn nhau của từ "consistency" trong ngành: replica consistency và eventual consistency ở Chương 5, consistent hashing ở Chương 6, linearizability ở Chương 9, và chữ C của ACID ở đây. Chúng không nói về cùng một thứ.
Vì sao vẫn hữu ích
Dù chữ C lạc chỗ, ý đứng sau nó thì không: atomicity và isolation tồn tại chính là để giúp ứng dụng giữ được bất biến của nó. Nói cách khác, ba chữ kia là công cụ; chữ C là mục tiêu mà bạn đặt ra.
-
Isolation & durability · promises with fine print
Hai lời hứa kèm điều kiệnTwo promises with conditions attached
Isolation hứa rằng các giao dịch chạy đồng thời được cách ly khỏi nhau — như thể chúng nối đuôi. Nhưng đó là định nghĩa của mức serializable, mà hầu hết hệ không chạy ở mức ấy vì cái giá hiệu năng. Durability hứa dữ liệu đã commit thì không mất; trên một nút đơn nó nghĩa là ghi xuống đĩa kèm write-ahead log, trong hệ nhân bản là sao chép sang các nút khác. Kleppmann nhấn rằng không có durability tuyệt đối: đĩa hỏng, firmware có bug, toàn bộ nút mất điện cùng lúc, bản sao lỗi thời — mọi kỹ thuật chỉ giảm rủi ro1.
Hai lời hứa này nghe chắc nịch nhưng đều có chữ nhỏ ở dưới. "Không giẫm chân nhau" thì phần lớn hệ chỉ làm được một phần, vì làm đủ thì chậm. "Ghi rồi là còn mãi" thì đúng trong hầu hết trường hợp, nhưng ổ cứng vẫn có thể hỏng. Nhớ nhé: an toàn là chuyện nhiều lớp, không có lớp nào chắc 100%.
Isolation promises that concurrent transactions are shielded from each other — as if they ran one after another. But that is the definition of serializable, and most systems do not run there because of the performance cost. Durability promises committed data will not be lost; on a single node that means writing to disk with a write-ahead log, in a replicated system copying to other nodes. Kleppmann stresses there is no absolute durability: disks fail, firmware has bugs, whole nodes lose power at once, replicas lag — every technique only reduces the risk1.
Cách đọc đúngCả hai đều là mệnh đề có điều kiện. Câu hỏi đúng không phải "có isolation không" mà "ở mức nào", không phải "có bền không" mà "bền trước những sự cố nào".Ý chínhHỏi "an toàn tới đâu", đừng hỏi "có an toàn không".How to read itBoth are conditional claims. The right question is not "is there isolation" but "at what level", not "is it durable" but "durable against which failures".Vì sao isolation bị hạ cấp
Cô lập tuần tự đúng nghĩa hiếm khi là mặc định. Theo bảng kiểm thử Hermitage, PostgreSQL, Oracle và SQL Server đều mặc định ở read committed, còn MySQL/InnoDB mặc định ở cái nó gọi là repeatable read5910. Nghĩa là: nếu bạn không chủ động đổi, bạn đang chạy ở mức yếu.
Durability nhiều lớp
Không kỹ thuật nào đủ một mình: ghi đĩa chống mất điện tiến trình nhưng không chống đĩa hỏng; nhân bản chống đĩa hỏng nhưng không chống bug lan ra mọi bản sao; bản sao lưu chống bug nhưng có độ trễ. Thực tế là xếp chồng nhiều lớp và chấp nhận xác suất còn lại.
-
Single-object vs multi-object operations
Một đối tượng hay nhiều đối tượng?One object, or many?
Gần như mọi kho dữ liệu đều đảm bảo nguyên tử và cô lập ở mức một đối tượng: ghi một tài liệu JSON lớn không để lại nửa tài liệu, một phép tăng bộ đếm hay compare-and-set trên một khóa là không thể chia nhỏ. Nhưng đó không phải giao dịch theo nghĩa thông thường. Giao dịch đa đối tượng mới là cái nhiều hệ phi quan hệ bỏ đi — và ta cần nó khi: có khóa ngoại giữa các bảng, có dữ liệu phi chuẩn hóa bị nhân bản ở nhiều chỗ, hoặc có chỉ mục thứ cấp phải cập nhật cùng nhịp với hàng dữ liệu1.
Sửa một ô dữ liệu thì hầu như hệ nào cũng làm gọn ghẽ. Khó là khi phải sửa nhiều ô cùng lúc mà không được lệch nhau: trừ tiền ở ô này thì phải cộng ở ô kia. Nhớ nhé: một ô thì dễ, nhiều ô mới cần "giao dịch" thật.
Almost every datastore guarantees atomicity and isolation at the level of a single object: writing a large JSON document never leaves half of it, and an increment or compare-and-set on one key is indivisible. But that is not a transaction in the usual sense. It is multi-object transactions that many non-relational systems dropped — and you need them when there are foreign keys between tables, denormalised data duplicated in several places, or secondary indexes that must be updated in step with the row1.
Câu hỏi kiểm traKhi một hệ quảng cáo "thao tác nguyên tử", hãy hỏi: nguyên tử trên một khóa, hay trên nhiều khóa? Khoảng cách giữa hai câu trả lời là toàn bộ chương này.Ý chínhHỏi cho rõ: nguyên tử một ô, hay nhiều ô một lúc?The test questionWhen a system advertises "atomic operations", ask: atomic on one key, or across several? The gap between those two answers is this whole chapter.Vì sao nhiều hệ bỏ giao dịch
Kleppmann chỉ ra rằng làn sóng NoSQL cuối những năm 2000 bỏ giao dịch đa đối tượng vì nó khó hiện thực trên kiến trúc phân mảnh và có chi phí hiệu năng — chứ không phải vì giao dịch là ý tưởng sai. Ông dành cả chương để chống lại cách đọc "giao dịch và co giãn là hai thứ loại trừ nhau".
Không phải mọi thứ cần giao dịch
Ông cũng nói rõ chiều ngược lại: nhiều ứng dụng thật sự sống tốt không cần giao dịch đa đối tượng, đặc biệt khi mô hình dữ liệu gom mọi thứ cần sửa cùng lúc vào một tài liệu. Lựa chọn ở đây là đánh đổi, không phải giáo điều.
Tên gọi không phải là bảo đảmThe label is not the guarantee
Cô lập tuần tự là bảo đảm sạch nhất, nhưng đắt — nên hầu hết cơ sở dữ liệu mặc định ở một mức yếu hơn, chặn một số race condition và để lọt số còn lại1. Rắc rối là ở tên gọi. Chuẩn ANSI SQL định nghĩa các mức cô lập qua ba "hiện tượng" — dirty read, non-repeatable read, phantom9 — và Berenson cùng cộng sự đã chỉ ra ngay từ 1995 rằng những định nghĩa đó không đủ để đặc tả nhiều mức cô lập phổ biến, kể cả cách hiện thực bằng khóa của chính chúng8. Kết quả là mỗi sản phẩm dùng cùng một cái tên cho những bảo đảm khác nhau.
Muốn thật an toàn thì phải xếp hàng từng người một — nhưng như thế chậm. Nên cơ sở dữ liệu cho bạn chọn mấy nấc "nhanh hơn nhưng hở hơn". Rắc rối là mỗi hãng gọi các nấc ấy bằng những cái tên na ná nhau mà nghĩa lại khác. Nhớ nhé: cùng một cái tên chưa chắc là cùng một lời hứa.
Serializable isolation is the cleanest guarantee, but it is expensive — so most databases default to something weaker that blocks some race conditions and lets the rest through1. The trouble is the naming. The ANSI SQL standard defines isolation levels through three "phenomena" — dirty read, non-repeatable read, phantom9 — and Berenson et al. showed back in 1995 that those definitions fail to characterise several popular isolation levels, including their own standard locking implementations8. The result: each product uses the same name for a different guarantee.
Mức cô lập là một danh sách những race condition được chặn, không phải một thang điểm an toàn. Read committed chặn dirty read và dirty write. Snapshot isolation chặn thêm read skew. Không mức nào trong hai mức đó chặn write skew. Và tùy hệ, lost update có thể được chặn hoặc không — ngay cả khi cái tên trên nhãn là giống nhau5.
Hãy hình dung mỗi mức cô lập là một cái rổ có lỗ. Rổ thưa thì nước chảy nhanh nhưng lọt nhiều; rổ dày thì chậm mà giữ được nhiều. Điều bạn cần biết là lỗ nào còn hở, chứ không phải cái tên in trên rổ. Nhớ nhé: hỏi "còn lọt lỗi gì" chứ đừng hỏi "có an toàn không".
An isolation level is a list of race conditions it blocks, not a safety score. Read committed blocks dirty reads and dirty writes. Snapshot isolation additionally blocks read skew. Neither blocks write skew. And depending on the system, lost updates may or may not be blocked — even when the name on the label is identical5.
Vì sao chuẩn lại mơ hồ đến thế? Berenson và cộng sự giải thích rằng cách định nghĩa qua hiện tượng của ANSI SQL-92 để lọt nhiều cách diễn giải, và họ phải đưa ra định nghĩa chặt hơn cùng những hiện tượng mới — trong đó snapshot isolation được định nghĩa chính thức, dù chuẩn SQL không hề có nấc nào mang tên ấy8. Tài liệu PostgreSQL nói thẳng khoảng cách này: repeatable read của họ không cho phép phantom read — mạnh hơn mức chuẩn đòi hỏi — và điều đó vẫn hợp chuẩn, vì chuẩn chỉ quy định mức bảo vệ tối thiểu9. Nói cách khác: chuẩn là sàn, không phải hợp đồng.
-
Read committed · no dirty reads, no dirty writes
Nấc thấp nhất còn dùng đượcThe lowest usable rung
Read committed hứa đúng hai điều: bạn chỉ đọc dữ liệu đã commit (không dirty read), và bạn chỉ ghi đè dữ liệu đã commit (không dirty write). Cách hiện thực thông thường: ghi thì giữ khóa hàng tới khi kết thúc giao dịch, còn đọc thì không dùng khóa — cơ sở dữ liệu giữ cả giá trị cũ lẫn giá trị mới và trả giá trị cũ cho tới khi giao dịch ghi commit1. PostgreSQL mô tả chính xác điều này: một câu SELECT chỉ thấy dữ liệu đã commit trước khi câu lệnh đó bắt đầu9.
Nấc này chặn hai điều đơn giản: bạn không đọc được bài viết dở của người khác, và bạn không đè lên bài viết dở của người khác. Thế thôi. Nó chưa hứa gì về chuyện bạn đọc hai lần ra hai kết quả khác nhau. Nhớ nhé: nấc thấp nhất chỉ chặn chuyện "viết dở", chưa chặn chuyện "đổi giữa chừng".
Read committed promises exactly two things: you only read committed data (no dirty reads), and you only overwrite committed data (no dirty writes). The usual implementation: writes hold a row lock until the transaction ends, while reads take no lock — the database keeps both the old and the new value and serves the old one until the writing transaction commits1. PostgreSQL describes precisely this: a SELECT sees only data committed before that statement began9.
Vì sao không khóa khi đọcNếu đọc cũng phải chờ khóa, một giao dịch ghi dài sẽ chặn mọi người đọc — trải nghiệm rất tệ. Nguyên tắc "người đọc không chặn người ghi, người ghi không chặn người đọc" là lý do nấc này nhanh.Ý chínhNgười đọc không phải xếp hàng chờ người viết — nhờ vậy mới nhanh.Why reads take no lockIf reads had to wait for locks, one long write would block every reader — a terrible experience. The principle "readers never block writers and writers never block readers" is why this rung is fast.Dirty write
Ít được nói tới hơn dirty read nhưng tai hại không kém: nếu giao dịch B ghi đè lên giá trị mà giao dịch A đã ghi nhưng chưa commit, kết quả có thể là một sự pha trộn không tương ứng với bất kỳ thứ tự chạy nào. Khóa hàng cho tới khi kết thúc giao dịch là cách chặn kinh điển.
Cái nó không chặn
PostgreSQL nêu rõ ở read committed, hai câu SELECT liên tiếp trong cùng một giao dịch có thể thấy dữ liệu khác nhau9. Đó chính là cửa ngõ dẫn tới hiện tượng tiếp theo — và là lý do snapshot isolation ra đời.
-
Read skew · the bank balance seen mid-transfer
Nhìn hai lần, thấy hai thế giớiLook twice, see two worlds
Kleppmann minh họa bằng một ví dụ ngân hàng: Alice có tiền chia đều trong hai tài khoản, và đúng lúc một giao dịch chuyển tiền từ tài khoản này sang tài khoản kia đang chạy, cô mở màn hình xem số dư. Truy vấn thứ nhất chạy trước khi khoản tiền được cộng vào, truy vấn thứ hai chạy sau khi nó đã bị trừ đi — tổng hiện ra bị thiếu đúng khoản đang chuyển, như thể tiền bốc hơi1. Đây là read skew (chuẩn gọi là non-repeatable read): không có gì bị mất thật, nhưng một cái nhìn đã bắt được trạng thái giữa chừng.
Tưởng tượng bạn đếm kẹo trong hai cái hộp, đúng lúc có người đang bê kẹo từ hộp này sang hộp kia. Bạn đếm hộp thứ nhất trước khi kẹo tới, đếm hộp thứ hai sau khi kẹo đi — thế là bạn tưởng kẹo bị mất. Thật ra chẳng mất gì cả, chỉ là bạn nhìn hai thời điểm khác nhau. Nhớ nhé: muốn đếm đúng thì phải chụp một tấm ảnh, đừng nhìn từng chỗ một.
Kleppmann illustrates with a banking example: Alice's money is split evenly across two accounts, and just as a transfer between them is running, she opens her balance screen. The first query runs before the money lands, the second after it has been deducted — the total comes up short by exactly the amount in flight, as if it had evaporated1. This is read skew (the standard calls it a non-repeatable read): nothing is actually lost, but one view caught an intermediate state.
Khi nó không thể chấp nhậnVới màn hình số dư, chờ vài giây rồi tải lại là xong. Nhưng sao lưu và truy vấn phân tích chạy hàng giờ trên cả cơ sở dữ liệu thì không có "tải lại" — một bản sao lưu bị lệch sẽ biến sự cố tạm thời thành hỏng hóc vĩnh viễn.Ý chínhNhìn nhầm một giây thì tải lại là xong; nhưng sao lưu nhầm thì cái sai được lưu lại mãi.When it's unacceptableFor a balance screen, waiting and reloading fixes it. But backups and analytic queries run for hours across the whole database, and there is no "reload" — a skewed backup turns a momentary glitch into permanent damage.Vì sao read committed không đủ
Ở read committed, mỗi câu lệnh lấy một ảnh chụp mới9. Một giao dịch gồm nhiều câu lệnh vì thế nhìn qua nhiều thời điểm khác nhau. Muốn cả giao dịch nhìn một thời điểm duy nhất, phải nâng lên nấc trên.
Ba ca kinh điển cần ảnh chụp
Kleppmann liệt kê những loại tải công việc không chịu được read skew: sao lưu, truy vấn phân tích và kiểm tra tính toàn vẹn. Điểm chung: chúng đọc rất nhiều dữ liệu trong một khoảng thời gian dài, nên xác suất bắt phải một trạng thái giữa chừng gần như chắc chắn.
-
Snapshot isolation & MVCC · one consistent picture
Ảnh chụp nhất quán và MVCCThe consistent snapshot, and MVCC
Giải pháp là snapshot isolation: mỗi giao dịch đọc từ một ảnh chụp nhất quán của cơ sở dữ liệu tại thời điểm nó bắt đầu, bất kể dữ liệu thay đổi thế nào sau đó. Hiện thực phổ biến là multi-version concurrency control (MVCC): CSDL giữ nhiều phiên bản đã commit của mỗi đối tượng, và mỗi giao dịch chỉ nhìn các phiên bản phù hợp với ảnh chụp của mình1. Tài liệu PostgreSQL nói thẳng: mức Repeatable Read của họ "được hiện thực bằng một kỹ thuật mà tài liệu học thuật và một số sản phẩm khác gọi là Snapshot Isolation"9. MySQL/InnoDB thì mặc định ở cái nó cũng gọi là repeatable read, nơi các lần đọc nhất quán trong cùng giao dịch đọc ảnh chụp do lần đọc đầu tiên thiết lập10.
Thay vì nhìn từng chỗ một, cơ sở dữ liệu chụp một tấm ảnh lúc bạn bắt đầu, rồi cho bạn xem tấm ảnh đó suốt từ đầu đến cuối. Người khác vẫn được sửa dữ liệu thật, nhưng ảnh của bạn không đổi. Cách làm là giữ lại nhiều bản cũ của mỗi món dữ liệu. Nhớ nhé: muốn đếm đúng thì chụp ảnh rồi hãy đếm.
The fix is snapshot isolation: each transaction reads from one consistent snapshot of the database as of the moment it began, whatever changes afterwards. The common implementation is multi-version concurrency control (MVCC): the database keeps several committed versions of each object, and each transaction sees only the versions belonging to its own snapshot1. The PostgreSQL manual says it outright: their Repeatable Read level "is implemented using a technique known in academic database literature and in some other database products as Snapshot Isolation"9. MySQL/InnoDB defaults to what it also calls repeatable read, where consistent reads within a transaction read the snapshot established by the first read10.
Bẫy tên gọiChuẩn SQL không có nấc nào tên là "snapshot isolation". Vì thế mỗi hãng nhét nó vào một cái tên sẵn có — và bạn phải tra tài liệu của chính hệ đó để biết mình đang ở đâu.Ý chínhCùng một kỹ thuật nhưng mỗi hãng gọi một tên khác nhau — phải đọc tài liệu của đúng hãng đó.The naming trapThe SQL standard has no level called "snapshot isolation". So each vendor files it under some existing name — and you must read that system's own docs to know where you stand.Cùng tên, khác bảo đảm
Bộ kiểm thử Hermitage của Kleppmann chạy thẳng các kịch bản bất thường lên từng hệ và xếp loại: cái PostgreSQL gọi "repeatable read" đạt đúng snapshot isolation; cái MySQL/InnoDB gọi "repeatable read" chỉ đạt tới monotonic atomic view — yếu hơn một bậc5. Cùng một chuỗi ký tự trong câu SET TRANSACTION, hai lời hứa khác nhau.
Cái giá của nhiều phiên bản
MVCC phải dọn rác: những phiên bản cũ không còn giao dịch nào cần tới phải được thu hồi, nếu không bảng phình ra. Một giao dịch đọc mở quá lâu sẽ giữ cả một đoàn phiên bản cũ sống dai — đây là nguyên nhân kinh điển của hiện tượng phình bảng trong các hệ MVCC.
-
Preventing lost updates · four routes, unevenly supported
Chống mất cập nhật: bốn con đườngPreventing lost updates: four routes
Lost update xảy ra với chu trình đọc – sửa – ghi: hai giao dịch cùng đọc một giá trị, cùng tính ra giá trị mới, cùng ghi — và một trong hai bị đè mất. Kleppmann nêu bốn cách chống1: (1) thao tác nguyên tử, để CSDL tự làm cả chu trình trong một câu lệnh; (2) khóa tường minh, dùng SELECT … FOR UPDATE để giữ chỗ trước khi sửa12; (3) compare-and-set, chỉ ghi nếu giá trị chưa đổi kể từ lúc đọc; và (4) phát hiện tự động, để CSDL bắt lỗi rồi buộc thử lại.
Hai bạn cùng mở một tờ giấy ghi "5 cái kẹo", cùng viết đè thành "6", vậy là một bạn cộng thừa mà không ai biết. Cách chữa: hoặc bảo máy tự cộng giúp, hoặc giữ tờ giấy lại khi đang sửa, hoặc kiểm tra "nó còn là 5 không" trước khi viết. Nhớ nhé: đọc rồi mới ghi là chỗ dễ mất công nhất.
A lost update happens in a read-modify-write cycle: two transactions read the same value, both compute a new one, both write — and one of them is overwritten. Kleppmann lists four defences1: (1) atomic operations, letting the database do the whole cycle in one statement; (2) explicit locking, using SELECT … FOR UPDATE to claim the row before changing it12; (3) compare-and-set, writing only if the value has not changed since you read it; and (4) automatic detection, letting the database catch it and force a retry.
Chỗ dễ sai nhấtCách (4) chỉ dùng được nếu hệ của bạn thật sự phát hiện. Theo bảng Hermitage, ở PostgreSQL mức repeatable read và Oracle mức serializable, mất cập nhật bị chặn; ở MySQL/InnoDB mức repeatable read thì không5.Ý chínhĐừng trông chờ máy tự bắt lỗi giúp — có hệ bắt, có hệ không.The easiest mistakeRoute (4) only works if your system actually detects it. Per the Hermitage table, PostgreSQL's repeatable read and Oracle's serializable prevent lost updates; MySQL/InnoDB's repeatable read does not5.Thông điệp lỗi có thật
PostgreSQL hủy giao dịch repeatable read đụng độ bằng thông báo "could not serialize access due to concurrent update"9; Oracle hủy giao dịch serializable tương tự bằng ORA-08177: can't serialize access for this transaction11. Ứng dụng phải bắt và thử lại — nếu không, cách (4) chỉ chuyển một lỗi âm thầm thành một lỗi ồn ào.
Khi có nhân bản
Trong hệ đa-leader hay không-leader, khóa và compare-and-set mất hiệu lực vì không còn một bản dữ liệu duy nhất để khóa. Ở đó, cách tiếp cận là cho phép ghi đồng thời rồi hợp nhất các phiên bản xung đột sau — chủ đề của Chương 5. Kleppmann lưu ý last-write-wins là cách hợp nhất dễ gây mất cập nhật nhất, dù nó là mặc định ở nhiều hệ.
Cùng một cái tên, ba lời hứa khác nhauOne name, three different promises
PostgreSQL · "repeatable read"PostgreSQL · "repeatable read"
Là snapshot isolation đúng nghĩa, và mạnh hơn chuẩn đòi hỏi: nó không cho phép phantom read. Tài liệu còn ghi rõ trước phiên bản 9.1, yêu cầu serializable cho ra đúng hành vi này9.
Genuine snapshot isolation, and stronger than the standard requires: it does not allow phantom reads. The manual also records that before version 9.1, requesting serializable gave exactly this behaviour9.
Oracle · "serializable"Oracle · "serializable"
Cái nhãn nói serializable, nhưng các kịch bản kiểm thử của Hermitage xếp nó là snapshot isolation5. Tài liệu Oracle cho thấy cơ chế: giao dịch đọc nhất quán ở mức giao dịch, và ghi vào hàng bị commit sau khi nó bắt đầu thì thất bại với ORA-0817711.
The label says serializable, but Hermitage's test scenarios classify it as snapshot isolation5. Oracle's own documentation shows the mechanism: transaction-level read consistency, and a write to a row committed after the transaction began fails with ORA-0817711.
MySQL/InnoDB · "repeatable read"MySQL/InnoDB · "repeatable read"
Cùng tên với PostgreSQL nhưng yếu hơn: tài liệu MySQL mô tả các lần đọc nhất quán dùng ảnh chụp của lần đọc đầu tiên10, còn Hermitage xếp nó là monotonic atomic view và ghi nhận mất cập nhật không được chặn5.
Same name as PostgreSQL's, weaker guarantee: MySQL's docs describe consistent reads using the snapshot of the first read10, while Hermitage classifies it as monotonic atomic view and records that lost updates are not prevented5.
Cái mà ảnh chụp không cứu đượcWhat the snapshot cannot save you from
Đây là phần tinh vi nhất của chương, và cũng là chỗ dễ đọc ẩu nhất. Write skew không phải dirty write, cũng không phải lost update: trong cả hai loại kia, hai giao dịch cùng ghi vào một đối tượng. Ở write skew, hai giao dịch đọc cùng một tập đối tượng chồng lấn rồi mỗi bên cập nhật một đối tượng khác nhau — nên không có gì để khóa, và kết quả kết hợp phá vỡ một bất biến mà từng giao dịch riêng lẻ vẫn giữ được1.
Hai bạn cùng nhìn vào một tình huống, ai cũng thấy "còn đủ người, mình rút lui được", rồi mỗi bạn tự rút. Từng quyết định đều hợp lý, nhưng cộng lại thì chẳng còn ai. Điều đáng sợ là hai bạn không sửa cùng một chỗ nên máy không thấy gì bất thường. Nhớ nhé: lỗi này không nằm ở chỗ ghi, mà nằm ở chỗ đọc.
This is the subtlest part of the chapter, and the easiest to read carelessly. Write skew is not a dirty write, nor a lost update: in both of those, two transactions write to the same object. In write skew, two transactions read the same overlapping set of objects and then each updates a different object — so there is nothing to lock, and the combined result breaks an invariant that each transaction alone would have preserved1.
Công thức của write skew: mỗi giao dịch chạy một truy vấn tìm kiếm (SELECT theo một điều kiện), lấy kết quả ra để quyết định, rồi ghi một thay đổi làm đổi tiền đề của truy vấn kia. Vì hai bên ghi vào hai hàng khác nhau, ảnh chụp nhất quán không thấy có gì đụng độ — và cả hai cùng commit1.
Công thức là: nhìn – quyết định – làm. Hai người cùng nhìn một lúc, cùng quyết định dựa trên cái mình thấy, rồi mỗi người làm một việc khác nhau. Cái mình nhìn thấy đã bị việc của người kia làm cho sai — nhưng lúc nhìn thì chưa. Nhớ nhé: quyết định đúng dựa trên tin cũ vẫn ra kết quả sai.
The write-skew recipe: each transaction runs a search query (a SELECT over some condition), uses the result to decide, then writes a change that invalidates the other's premise. Because the two write to different rows, the consistent snapshot sees no conflict — and both commit1.
Đừng trộn write skew với phantom: chúng là hai khái niệm khác nhau nhưng hay đi cùng nhau. Phantom là hiện tượng — một lệnh ghi ở giao dịch này làm đổi kết quả của một truy vấn tìm kiếm ở giao dịch khác. Write skew là bất thường ở mức ứng dụng mà phantom thường gây ra. Snapshot isolation chặn được phantom trong các truy vấn chỉ đọc, nhưng không chặn được phantom trong chu trình đọc-rồi-ghi — đó chính là kẽ hở1. Chuẩn ANSI SQL có nhắc tên phantom, nhưng theo Berenson và cộng sự, cách định nghĩa bằng hiện tượng của chuẩn quá mơ hồ để phân biệt được những ca này8.
-
The on-call doctors
Ca trực bác sĩ: cả hai cùng rútOn-call doctors: both step away
Ví dụ trung tâm của Kleppmann: bệnh viện quy định luôn phải có ít nhất một bác sĩ trực. Alice và Bob cùng đang trực và cùng thấy không khỏe. Hai người cùng lúc bấm xin nghỉ. Giao dịch của Alice đếm số bác sĩ đang trực, thấy 2, kết luận "bỏ mình vẫn còn một người", rồi cập nhật hàng của Alice. Giao dịch của Bob làm y hệt trên ảnh chụp của nó, thấy 2, rồi cập nhật hàng của Bob. Hai hàng khác nhau, không đụng độ, cả hai commit — và ca trực trống trơn1.
Bệnh viện có luật: lúc nào cũng phải còn ít nhất một bác sĩ trực. Alice và Bob đang trực, cả hai cùng mệt. Alice nhìn: "còn hai người, mình về được". Bob cũng nhìn đúng lúc đó: "còn hai người, mình về được". Thế là cả hai cùng về, chẳng còn ai. Nhớ nhé: hai người cùng nhìn một lúc rồi cùng rút lui thì luật bị phá.
Kleppmann's central example: a hospital requires that at least one doctor is always on call. Alice and Bob are both on call and both feel unwell. They click to request leave at the same moment. Alice's transaction counts the doctors on call, sees 2, concludes "one will remain without me", and updates Alice's row. Bob's transaction does the same on its own snapshot, sees 2, and updates Bob's row. Different rows, no collision, both commit — and the shift is empty1.
Vì sao ảnh chụp thuaBất biến "≥ 1 bác sĩ trực" trải trên nhiều hàng. Ảnh chụp bảo vệ từng hàng khỏi bị sửa lén, nhưng không bảo vệ được một mệnh đề tính trên cả tập hàng.Ý chínhLuật đếm cả nhóm thì không thể canh bằng cách giữ từng người một.Why the snapshot losesThe invariant "at least one doctor on call" spans many rows. A snapshot protects each row from being changed behind your back, but not a claim computed across the whole set.Cách chữa đúng
Với ca này, hai lối thoát đáng tin: nâng lên mức serializable, hoặc khóa tường minh mọi hàng mà quyết định phụ thuộc vào — SELECT … FOR UPDATE trên tất cả bác sĩ đang trực12. Chỉ khóa hàng mình sắp sửa thì vô dụng, vì hai giao dịch sửa hai hàng khác nhau.
Vì sao dễ lọt lưới kiểm thử
Bất thường này chỉ hiện ra khi hai giao dịch chồng nhau đúng một khoảng thời gian rất hẹp. Kiểm thử tuần tự sẽ luôn đúng; chỉ tải thật, đồng thời thật mới lộ. Đó là lý do write skew thường được phát hiện bởi khách hàng chứ không phải bởi CI.
-
Phantoms · locking something that does not exist yet
Khóa một thứ chưa tồn tạiLocking what isn't there yet
Kleppmann gọi hiệu ứng "một lệnh ghi ở giao dịch này làm đổi kết quả truy vấn tìm kiếm ở giao dịch kia" là phantom, và liệt kê một loạt ca cùng một khuôn: đặt phòng họp (hai người cùng kiểm tra khung giờ trống rồi cùng đặt), game nhiều người (hai nước đi cùng dời quân vào một ô), đăng ký tên người dùng (hai người cùng kiểm tra tên chưa có rồi cùng tạo), và chống thấu chi tài khoản (hai lệnh chi cùng thấy đủ số dư)1. Điểm chung: thứ gây xung đột là hàng chưa tồn tại, nên không có gì để khóa.
Bạn không thể giữ chỗ cho một cái ghế chưa được kê ra. Hai người cùng xem lịch thấy phòng họp 3 giờ chiều còn trống, cùng đặt — và phòng bị đặt hai lần. Cái hai người "tranh nhau" là một dòng đặt phòng chưa hề tồn tại lúc họ nhìn. Nhớ nhé: chỗ trống cũng cần được canh, không chỉ chỗ đã có người.
Kleppmann calls the effect "a write in one transaction changing the result of a search query in another" a phantom, and lists a family of cases with the same shape: meeting-room booking (two people check a slot is free and both book it), multiplayer games (two moves put pieces on the same square), claiming a username (two people check the name is free and both create it), and preventing account overdraft (two payments both see enough balance)1. The common thread: the thing in conflict is a row that does not exist yet, so there is nothing to lock.
Ranh giới cần giữPhantom là cơ chế; write skew là hậu quả ở mức ứng dụng. Snapshot isolation chặn phantom cho truy vấn chỉ đọc, nhưng không chặn khi truy vấn ấy dẫn tới một lệnh ghi.Ý chính"Bóng ma" là cách lỗi xảy ra; "lệch ghi" là hậu quả bạn nhìn thấy.The boundary to keepA phantom is the mechanism; write skew is the application-level consequence. Snapshot isolation blocks phantoms for read-only queries, but not when the query leads to a write.Khi CSDL tự lo được
Ca "tên người dùng trùng" có lối thoát gọn: đặt một ràng buộc unique lên cột tên. Giao dịch thứ hai sẽ bị từ chối ngay ở tầng CSDL. Đây là ví dụ hiếm hoi mà bất biến của ứng dụng đủ hẹp để rơi đúng vào mảnh mà CSDL ép giúp được — đúng như phần chữ C ở trên đã nói.
Cả serializable cũng có chữ nhỏ
Tài liệu PostgreSQL cảnh báo rằng ngay ở mức serializable, bạn vẫn có thể gặp lỗi vi phạm ràng buộc unique do đụng độ giữa các giao dịch chồng nhau, kể cả sau khi đã chủ động kiểm tra khóa chưa tồn tại9. Nghĩa là ứng dụng vẫn phải xử lý lỗi, không chỉ xử lý serialization failure.
-
Materializing conflicts · an ugly last resort
Vật chất hóa xung độtMaterializing the conflict
Nếu không có hàng nào để khóa, một mẹo là tạo ra hàng đó trước. Với bài toán đặt phòng họp: điền sẵn vào bảng mọi tổ hợp (phòng × khung giờ) cho khoảng thời gian tới, rồi bắt mỗi giao dịch đặt phòng phải khóa đúng hàng tương ứng. Xung đột vốn là "bóng ma" nay trở thành đụng độ trên một hàng có thật. Kleppmann gọi đây là materializing conflicts và nói thẳng: đây là giải pháp xấu, khó và dễ sai, chỉ nên dùng khi mọi lựa chọn khác đã hết1.
Không giữ chỗ được cho cái ghế chưa kê? Thì kê sẵn hết ghế ra, đánh số, rồi ai muốn ngồi thì phải cầm đúng số ghế đó. Cách này chạy được, nhưng bạn phải kê rất nhiều ghế mà phần lớn chẳng ai ngồi. Nhớ nhé: mẹo này chữa được bệnh nhưng khá tốn kém, chỉ dùng khi hết cách.
If there is no row to lock, one trick is to create that row in advance. For the meeting-room problem: pre-fill a table with every (room × time-slot) combination for the coming period, then require each booking transaction to lock the matching row. What was a phantom conflict becomes a collision on a real row. Kleppmann calls this materializing conflicts and says it plainly: it is an ugly solution, hard to get right and error-prone, to be used only when nothing else works1.
Vì sao ông không thích nóNó rò rỉ chi tiết cơ chế đồng thời ra mô hình dữ liệu của ứng dụng: bảng khung giờ tồn tại không vì nghiệp vụ mà vì giới hạn của mức cô lập.Ý chínhBạn phải thêm dữ liệu vô nghĩa vào chỉ để máy có chỗ mà canh.Why he dislikes itIt leaks concurrency-control detail into the application's data model: the time-slot table exists not for the business, but for the limits of the isolation level.Thứ tự nên thử
Kleppmann xếp rõ thứ tự ưu tiên: nếu được thì dùng mức serializable; nếu không thì khóa tường minh toàn bộ tiền đề bằng SELECT … FOR UPDATE12; và chỉ khi cả hai bất khả thi mới tới lượt materializing conflicts.
Ranh giới bảo trì
Bảng vật chất hóa phải được duy trì: sinh thêm khung giờ cho tương lai, dọn khung giờ quá khứ, giữ đồng bộ khi thêm phòng mới. Mỗi công việc bảo trì ấy là một chỗ có thể quên — và khi quên thì bất biến lại vỡ trong im lặng.
-
Write skew in the wild
Không phải chuyện trong phòng thí nghiệmNot a laboratory curiosity
Dễ nghĩ write skew là ví dụ giáo khoa. Không phải. Jorwekar, Fekete, Ramamritham và Sudarshan đã xây một phương pháp phân tích tĩnh mã ứng dụng để tự động phát hiện các bất thường của snapshot isolation, và áp dụng nó lên các ứng dụng doanh nghiệp thật đang chạy trên nền snapshot isolation17. Bài báo ấy nằm trong chính danh mục tham chiếu Chương 7 của Kleppmann4 — dấu hiệu rằng vấn đề đủ phổ biến để đáng xây công cụ riêng mà tìm.
Đây không phải ví dụ bịa ra cho vui. Người ta đã phải viết hẳn công cụ đi dò mã nguồn của các phần mềm doanh nghiệp thật để tìm loại lỗi này — vì nó xuất hiện đủ nhiều. Nhớ nhé: lỗi khó thấy không có nghĩa là lỗi hiếm gặp.
It is tempting to think write skew is a textbook toy. It is not. Jorwekar, Fekete, Ramamritham and Sudarshan built a method for statically analysing application code to automatically detect snapshot-isolation anomalies, and applied it to real enterprise applications running on snapshot isolation17. That paper sits in Kleppmann's own Chapter 7 reference list4 — a sign the problem is common enough to justify purpose-built tooling.
Rút raNếu ứng dụng của bạn chạy trên snapshot isolation (kể cả khi nhãn ghi "repeatable read" hay "serializable"), hãy đi tìm các chu trình đọc-quyết định-ghi trên nhiều hàng. Đó là nơi bất thường ẩn.Ý chínhHãy đi tìm những chỗ code "xem rồi mới quyết định" — đó là nơi dễ có lỗi nhất.The takeawayIf your application runs on snapshot isolation (even under a label saying "repeatable read" or "serializable"), go hunting for read-decide-write cycles that span several rows. That is where the anomalies hide.Dấu hiệu nhận biết trong mã
Khuôn mẫu đáng ngờ rất đều: một SELECT có WHERE lọc theo điều kiện nghiệp vụ, một câu if dựa trên kết quả đó, rồi một INSERT hoặc UPDATE vào hàng khác. Hễ thấy khuôn này là phải hỏi: nếu hai người chạy nó cùng lúc thì sao?
Vì sao không tự lộ
Khác với deadlock hay lỗi ràng buộc, write skew không sinh ra thông báo lỗi nào. Cả hai giao dịch đều commit thành công. Hậu quả chỉ hiện ra sau đó, ở dạng dữ liệu vi phạm một quy tắc nghiệp vụ — và lúc ấy rất khó truy ngược về nguyên nhân.
Ba thứ rất dễ trộn lẫnThree things people constantly conflate
Dirty writeDirty write
Hai giao dịch ghi vào cùng một đối tượng, và một bên đè lên giá trị mà bên kia chưa commit. Chặn được bằng khóa hàng giữ tới cuối giao dịch — read committed đã đủ1.
Two transactions write to the same object, and one overwrites a value the other has not yet committed. Blocked by row locks held to the end of the transaction — read committed already suffices1.
Lost updateLost update
Cũng là cùng một đối tượng, nhưng cả hai đều commit hợp lệ; cái mất là phần sửa của một bên trong chu trình đọc-sửa-ghi. Cần thao tác nguyên tử, khóa, CAS, hoặc phát hiện tự động1.
Also the same object, but both commit legitimately; what is lost is one side's modification in a read-modify-write cycle. Needs atomic operations, locks, CAS, or automatic detection1.
Write skewWrite skew
Hai đối tượng khác nhau. Không ai đè ai, không có lỗi nào được ném ra — chỉ có một bất biến trải trên nhiều hàng bị vỡ. Chỉ serializable hoặc khóa toàn bộ tiền đề mới chặn được1.
Two different objects. Nobody overwrites anybody, no error is raised — just an invariant spanning several rows quietly broken. Only serializable, or locking the entire premise, prevents it1.
Ba con đường, ba cái giáThree routes, three prices
Serializable isolation là bảo đảm mạnh nhất: kết quả của một tập giao dịch chạy đồng thời phải giống hệt như thể chúng đã chạy nối đuôi nhau theo một thứ tự nào đó. Có nó thì mọi bất thường ở ba phần trên — dirty read, read skew, lost update, write skew, phantom — biến mất cùng một lúc, và lập trình viên không còn phải nghĩ về race condition trong từng ca một1. Câu hỏi duy nhất còn lại là: hiện thực nó kiểu gì, và trả giá ở đâu. Kleppmann trình bày đúng ba con đường.
Cách an toàn nhất là bắt mọi việc xếp hàng thật sự: xong việc này mới tới việc kia. Làm được thế thì tất cả các lỗi ở trên biến mất hết. Chỉ có điều xếp hàng thì chậm — nên người ta nghĩ ra ba cách khác nhau để vừa xếp hàng vừa đỡ chậm. Nhớ nhé: an toàn nhất là xếp hàng, và mỗi cách xếp hàng có một cái giá riêng.
Serializable isolation is the strongest guarantee: the outcome of a set of concurrent transactions must be identical to them having run one after another in some order. With it, every anomaly from the three sections above — dirty reads, read skew, lost updates, write skew, phantoms — disappears at once, and the programmer no longer reasons about race conditions case by case1. The only remaining question is how to implement it, and where the price is paid. Kleppmann lays out exactly three routes.
Ba con đường khác nhau ở chỗ đặt cược. Serial execution cược rằng giao dịch đủ ngắn và dữ liệu đủ nhỏ để chạy một luồng. Two-phase locking cược bi quan: giả định sẽ có đụng độ, nên chặn trước bằng khóa. Serializable snapshot isolation cược lạc quan: cứ cho chạy, tới lúc commit mới kiểm xem tiền đề còn đúng không, sai thì hủy1.
Ba cách xếp hàng: một — chỉ cho một người làm mỗi lúc, xong mới tới lượt sau. Hai — ai đang dùng món gì thì giữ chặt món đó, người khác phải chờ. Ba — cứ để mọi người làm thoải mái, tới lúc nộp bài mới kiểm, ai bị chồng chéo thì làm lại. Nhớ nhé: chờ trước hay làm lại sau — chọn một.
The three routes differ in where they place the bet. Serial execution bets that transactions are short enough and the data small enough to run on one thread. Two-phase locking bets pessimistically: assume collisions, so block them in advance with locks. Serializable snapshot isolation bets optimistically: let everything run, and only at commit time check whether the premises still hold — aborting if not1.
Một lưu ý thuật ngữ mà Kleppmann nhấn mạnh: 2PL không phải 2PC. Two-phase locking là một thuật toán kiểm soát đồng thời trong một nút; two-phase commit là một giao thức commit nguyên tử trải trên nhiều nút, chủ đề của Chương 9. Hai chữ viết tắt gần giống nhau và cực kỳ hay bị trộn1. Một lưu ý thứ hai: "khả tuần tự hóa" nói về kết quả, không phải về thứ tự thời gian thật. Một tập giao dịch được coi là tuần tự hóa được nếu tồn tại một thứ tự nối đuôi cho ra đúng kết quả ấy — không cần biết thứ tự đó có khớp với thứ tự chúng thật sự bắt đầu hay không9.
-
Actual serial execution · one thread, no locks
Thực thi tuần tự thật sựActual serial execution
Con đường đơn giản nhất là bỏ hẳn tính đồng thời: chạy mỗi lần một giao dịch, trên một luồng duy nhất. Ý tưởng cũ nhưng chỉ khả thi gần đây nhờ hai thay đổi mà Kleppmann nêu: RAM đã đủ rẻ để nhiều tập dữ liệu OLTP nằm gọn trong bộ nhớ, và người ta nhận ra giao dịch OLTP thường ngắn, chỉ đụng ít bản ghi1. Kiến trúc này được Stonebraker và cộng sự lập luận trong bài "The End of an Architectural Era"18 và hiện thực trong H-Store19, dòng dõi trực tiếp của VoltDB20.
Cách chắc chắn nhất để không ai giẫm chân ai: mỗi lúc chỉ cho một người làm. Nghe có vẻ dở, nhưng nếu mỗi việc chỉ mất một khoảnh khắc rất ngắn và toàn bộ dữ liệu nằm sẵn trong bộ nhớ thì xếp hàng vẫn rất nhanh. Nhớ nhé: khi mỗi việc cực ngắn thì xếp hàng không còn là vấn đề.
The simplest route is to remove concurrency entirely: run one transaction at a time, on a single thread. An old idea made viable recently by two changes Kleppmann names: RAM is cheap enough that many OLTP datasets fit in memory, and OLTP transactions turned out to be short, touching few records1. This architecture was argued by Stonebraker et al. in "The End of an Architectural Era"18 and implemented in H-Store19, the direct ancestor of VoltDB20.
Điều kiện bắt buộcGiao dịch phải được đóng gói thành stored procedure và gửi trọn gói cho CSDL20. Nếu giao dịch phải chờ client hỏi đi hỏi lại qua mạng, một luồng duy nhất sẽ đứng chờ và cả hệ đứng theo.Ý chínhPhải gửi nguyên cả việc cho máy làm một lượt, đừng vừa làm vừa hỏi.The hard prerequisiteTransactions must be packaged as stored procedures and submitted whole20. If a transaction waits for round-trips from the client, the single thread stalls and the whole system stalls with it.Phân mảnh để mở rộng
Một luồng nghĩa là bị giới hạn bởi một lõi CPU. Cách mở rộng là phân mảnh dữ liệu và cho mỗi phân mảnh một luồng riêng, chạy độc lập20. Nhưng giao dịch nào phải chạm nhiều phân mảnh thì cần điều phối xuyên phân mảnh, và Kleppmann lưu ý nó chậm hơn giao dịch một phân mảnh rất nhiều — nên thông lượng thật phụ thuộc vào việc bạn phân mảnh khéo tới đâu.
Cái giá ẩn
Viết nghiệp vụ bằng stored procedure có tiếng xấu về khả năng bảo trì: ngôn ngữ thủ tục cũ kỹ, khó quản lý phiên bản, khó kiểm thử. Kleppmann ghi nhận các hệ hiện đại dùng ngôn ngữ phổ thông cho phần này, nhưng cái giá về quy trình phát triển vẫn có thật.
-
Two-phase locking · the pessimistic classic
Khóa hai pha: bi quan và cổ điểnTwo-phase locking: the pessimist's classic
Trong gần ba mươi năm, 2PL là cách hiện thực serializability. Nguyên tắc chặt hơn hẳn các nấc yếu: nhiều giao dịch được cùng đọc một đối tượng, nhưng hễ có ai muốn ghi thì phải độc quyền — và quan trọng nhất, người đọc chặn người ghi, người ghi chặn người đọc, khóa giữ cho tới khi giao dịch kết thúc1. Chính điều đó phá vỡ nguyên tắc "đọc không chặn ghi" vốn làm snapshot isolation nhanh, và đó là nguồn gốc mọi vấn đề hiệu năng của nó.
Cách thứ hai: ai đang dùng món gì thì giữ chặt món đó cho tới khi làm xong, người khác phải đứng chờ. Rất an toàn — nhưng nếu có một người làm lâu thì cả hàng dài phía sau đứng im. Nhớ nhé: giữ chặt thì an toàn, nhưng ai cũng phải chờ.
For nearly thirty years, 2PL was how serializability got implemented. The rule is far stricter than the weak rungs: several transactions may read the same object concurrently, but anyone wanting to write needs exclusive access — and crucially, readers block writers and writers block readers, with locks held until the transaction ends1. That breaks the "reads never block writes" principle that makes snapshot isolation fast, and it is the root of all its performance troubles.
Cái giá cụ thểThông lượng thấp hơn hẳn các nấc yếu, và độ trễ ở phần đuôi rất xấu: chỉ cần một giao dịch chậm giữ khóa là cả hàng phía sau bị kéo theo. Thêm vào đó là deadlock — CSDL phải phát hiện và hủy một bên, khiến ứng dụng phải thử lại.Ý chínhMột người làm chậm là cả hàng chậm theo — và đôi khi hai người khóa lẫn nhau, phải bắt một người làm lại.The concrete priceThroughput is much lower than at weak levels, and tail latency is very bad: one slow transaction holding a lock drags the whole queue behind it. Plus deadlocks — the database must detect them and abort one side, forcing the application to retry.Ai dùng 2PL
Hermitage ghi nhận MySQL/InnoDB và SQL Server ở mức "serializable" đạt đúng serializable5 — và với InnoDB, cơ chế đứng sau là khóa, chứ không phải cách lạc quan. Tài liệu MySQL mô tả cách nó khóa dải chỉ mục được quét bằng gap lock và next-key lock10.
2PL ≠ 2PC
Đây là cặp viết tắt gây nhầm nhiều nhất trong cả cuốn sách. Two-phase locking giải quyết cô lập trong một nút. Two-phase commit giải quyết atomicity xuyên nhiều nút. Không liên quan gì tới nhau ngoài chữ "hai pha"1.
-
Predicate locks & index-range locks
Khóa vị từ và khóa dải chỉ mụcPredicate locks and index-range locks
Khóa hàng không chặn được phantom, vì hàng gây xung đột chưa tồn tại. Lời giải cổ điển là khóa vị từ: thay vì khóa một đối tượng cụ thể, khóa mọi đối tượng thỏa một điều kiện tìm kiếm — kể cả những đối tượng chưa có mặt trong cơ sở dữ liệu. Khái niệm này có từ bài của Eswaran, Gray, Lorie và Traiger năm 197613. Vấn đề là nó đắt: phải đối chiếu mọi khóa vị từ đang giữ với mọi thao tác. Nên hầu hết hiện thực 2PL hạ xuống khóa dải chỉ mục — thô hơn nhưng rẻ hơn nhiều1.
Muốn giữ chỗ cho cái ghế chưa kê, ta không giữ cái ghế mà giữ cả khoảng không nơi ghế sẽ được kê. Cách này chắc chắn nhưng mất công tính toán, nên thực tế người ta giữ một vùng rộng hơn cần thiết cho nhanh. Nhớ nhé: giữ rộng hơn thì đơn giản hơn, chỉ là đôi khi chặn nhầm người vô can.
Row locks cannot block phantoms, because the conflicting row does not exist yet. The classic answer is a predicate lock: instead of locking a specific object, lock all objects matching a search condition — including objects not yet present in the database. The idea goes back to Eswaran, Gray, Lorie and Traiger in 197613. The problem is cost: every held predicate lock must be checked against every operation. So most 2PL implementations fall back to index-range locks — coarser, but far cheaper1.
Đánh đổiKhóa dải chỉ mục khóa rộng hơn mức cần — nó có thể chặn cả những giao dịch vốn không hề đụng độ. Đổi lại, chi phí duy trì thấp hơn hẳn và vẫn đủ để chặn phantom.Đánh đổiKhóa rộng thì rẻ và dễ, nhưng chặn oan vài người.The trade-offAn index-range lock locks more than strictly needed — it can block transactions that would never have collided. In exchange the bookkeeping is far cheaper, and it still stops phantoms.Hiện thực có thật
Tài liệu MySQL mô tả đúng cơ chế này: với các điều kiện tìm kiếm không phải khóa duy nhất, InnoDB khóa dải chỉ mục được quét bằng gap lock hoặc next-key lock để chặn các phiên khác chèn vào khoảng trống trong dải10. Đây là "index-range lock" của sách, gọi bằng tên riêng của MySQL.
Khi không có chỉ mục phù hợp
Nếu truy vấn không dùng được chỉ mục nào, hệ buộc phải lùi về khóa thô hơn nữa — tới mức khóa cả bảng. An toàn vẫn đúng, nhưng tính đồng thời sụp xuống gần bằng không. Đây là lý do chỉ mục ảnh hưởng tới cả hiệu năng lẫn mức chặn ở các hệ dùng 2PL.
-
Serializable snapshot isolation · optimistic, and young
SSI: lạc quan thay vì bi quanSSI: optimistic instead of pessimistic
Con đường thứ ba còn mới. Fekete và cộng sự chỉ ra năm 2005 rằng có thể làm cho snapshot isolation trở nên tuần tự hóa được bằng cách xử lý một lớp phụ thuộc đặc thù14; Cahill, Röhm và Fekete biến nó thành thuật toán thực dụng năm 200815; và Ports cùng Grittner đưa nó vào PostgreSQL, nơi nó trở thành mức serializable từ phiên bản 9.1169. Ý tưởng: giữ nguyên ảnh chụp nhất quán và không chặn ai cả, nhưng theo dõi các phụ thuộc đọc-ghi; tới lúc commit, nếu phát hiện tiền đề mà giao dịch đã dựa vào đã bị thay đổi, thì hủy nó1.
Cách thứ ba: cứ để mọi người làm thoải mái, không ai phải chờ ai. Nhưng máy âm thầm ghi lại "bạn này đã dựa vào thông tin kia". Tới lúc nộp bài, nếu thông tin đó đã bị người khác đổi mất, máy bắt bạn làm lại. Nhớ nhé: không chờ ai cả, nhưng đôi khi phải làm lại từ đầu.
The third route is young. Fekete et al. showed in 2005 that snapshot isolation could be made serializable by handling one particular class of dependency14; Cahill, Röhm and Fekete turned it into a practical algorithm in 200815; and Ports and Grittner brought it into PostgreSQL, where it became the serializable level from version 9.1169. The idea: keep the consistent snapshot and block nobody, but track read-write dependencies; at commit time, if a premise the transaction relied on has since changed, abort it1.
Vì sao nó hấp dẫnPostgreSQL nói rõ việc theo dõi này không gây chặn thêm so với repeatable read: các khóa vị từ của nó hiện ra dưới dạng SIReadLock và không thể gây deadlock9. Cái giá không nằm ở chờ, mà nằm ở tỉ lệ bị hủy.Ý chínhKhông ai phải chờ — đổi lại thỉnh thoảng có người phải làm lại.Why it appealsPostgreSQL states this monitoring introduces no blocking beyond repeatable read: its predicate locks appear as SIReadLock and cannot cause deadlocks9. The price is not waiting, it is the abort rate.Ứng dụng phải biết thử lại
PostgreSQL hủy giao dịch serializable bị đụng độ bằng thông báo "could not serialize access due to read/write dependencies among transactions", và mọi serialization failure đều trả về mã SQLSTATE 400019. Tài liệu nói thẳng: môi trường dùng kỹ thuật này phải có cách xử lý chung cho lỗi ấy, vì rất khó đoán trước giao dịch nào sẽ bị hủy.
Khi nào SSI thắng, khi nào thua
Kleppmann nêu đánh đổi cốt lõi: SSI tốt khi tranh chấp thấp và giao dịch ngắn, vì phần lớn giao dịch đi qua trót lọt. Khi tranh chấp cao, tỉ lệ hủy tăng và công sức bị vứt đi nhiều — lúc đó cách bi quan có thể lại hiệu quả hơn. Giao dịch càng dài thì càng dễ bị hủy, nên kỷ luật "giữ giao dịch thật ngắn" là điều kiện sống còn.
Ba ngộ nhận cuối chươngThree misreadings to leave behind
"Nhãn ghi serializable là tuần tự thật""If the label says serializable, it is serializable"
Sai. Các kịch bản kiểm thử của Hermitage xếp mức "serializable" của Oracle là snapshot isolation5, và tài liệu PostgreSQL ghi lại rằng trước 9.1, yêu cầu serializable ở PostgreSQL cũng chỉ cho ra hành vi repeatable read9. Phải tra tài liệu của chính hệ mình dùng, đúng phiên bản mình chạy.
Wrong. Hermitage's test scenarios classify Oracle's "serializable" as snapshot isolation5, and PostgreSQL's manual records that before 9.1, requesting serializable there also gave only repeatable-read behaviour9. Read your own system's docs, for the version you actually run.
"Snapshot isolation là đủ an toàn""Snapshot isolation is safe enough"
Sai. Ảnh chụp nhất quán chặn read skew nhưng để lọt write skew, và mất cập nhật thì tùy hệ mới được chặn5. Nếu ứng dụng có bất biến trải trên nhiều hàng, ảnh chụp không bảo vệ được bạn — cần serializable hoặc khóa tường minh toàn bộ tiền đề1.
Wrong. A consistent snapshot blocks read skew but lets write skew through, and whether lost updates are prevented depends on the system5. If your application has invariants spanning several rows, the snapshot will not protect you — you need serializable, or explicit locks over the entire premise1.
"Serializable chậm nên đừng dùng""Serializable is slow, so avoid it"
Quá vội. Câu đó đúng với 2PL, nơi mọi người phải chờ nhau. Với SSI thì cái giá đổi chỗ: không chặn thêm ai, nhưng có tỉ lệ hủy9. PostgreSQL còn lập luận rằng với một số môi trường, serializable là lựa chọn tốt nhất về hiệu năng so với việc tự đi khóa tay9.
Too hasty. That is true of 2PL, where everyone waits on everyone. With SSI the price moves: no extra blocking, but an abort rate9. PostgreSQL even argues that for some environments, serializable is the best performance choice compared with hand-rolled explicit locking9.
Soi giao dịch trong hệ của bạnAudit the transactions in your own system
Chương này không có công thức chung, chỉ có một kỷ luật: bỏ nhãn xuống và đi tra hành vi thật. Mấy câu dưới đây là bộ soi tối thiểu cho bất kỳ hệ nào bạn đang vận hành — chúng không hỏi hệ của bạn "mạnh" tới đâu, mà hỏi bạn có biết mình đang đứng ở nấc nào hay không.
Thử soi một hệ thật bằng mấy câu dưới. Không phải để chấm điểm, mà để xem bạn có thật sự biết hệ của mình đang chặn được lỗi nào không.
This chapter offers no universal recipe, only a discipline: put the label down and go look up the actual behaviour. The questions below are a minimum audit for any system you operate — they don't ask how "strong" your system is, but whether you know which rung you are standing on.
Giao dịch không phải một lời hứa duy nhất mà là một phổ các lời hứa, và cái tên dán trên mỗi nấc không đáng tin bằng tài liệu của chính hệ đó. ACID chỉ nói rõ được ba chữ; Consistency vẫn là việc của bạn. Ảnh chụp nhất quán cứu bạn khỏi read skew nhưng không cứu khỏi write skew. Và khi bạn quyết định bịt hết mọi kẽ hở, bạn không xóa được chi phí — bạn chỉ chọn trả nó ở đâu: chờ trước với 2PL, làm lại sau với SSI, hay chấp nhận ràng buộc kiến trúc với thực thi tuần tự.Giao dịch không phải một lời hứa duy nhất, mà là nhiều nấc an toàn khác nhau — và cái tên in trên nấc thường không nói thật. Máy lo giúp bạn vài việc, còn "thế nào là đúng" vẫn do bạn đặt ra. Nhớ nhé: muốn thật an toàn thì phải xếp hàng, và xếp hàng thì luôn phải trả giá — chờ trước, hoặc làm lại sau.A transaction is not one promise but a spectrum of promises, and the name on each rung is less trustworthy than that system's own documentation. ACID only pins down three letters; Consistency remains your job. A consistent snapshot saves you from read skew but not from write skew. And when you decide to close every hole, you do not erase the cost — you only choose where to pay it: waiting up front with 2PL, redoing work afterwards with SSI, or accepting architectural constraints with serial execution.