Anomaly & serializability — lost update, write skew, SSI
Các anomaly khi nhiều giao dịch đồng thời: lost update, write skew, phantom; serializability là chuẩn vàng, và cách đạt nó (2PL, SSI) cùng cái giá.
TL;DR: Khi nhiều giao dịch chạy đồng thời, chúng có thể phá nhau theo các kiểu gọi là anomaly. Ba kiểu quan trọng: lost update (hai giao dịch cùng đọc-sửa-ghi một giá trị, một bản ghi đè bản kia — mất một cập nhật), write skew (mỗi giao dịch đọc một trạng thái, quyết định dựa trên nó, rồi ghi — nhưng quyết định của chúng cộng lại phá một ràng buộc mà từng cái tưởng vẫn đúng), và phantom (một giao dịch ghi làm thay đổi tập kết quả của truy vấn giao dịch khác đang dựa vào). Chuẩn vàng chống mọi anomaly là serializability: đảm bảo kết quả y như thể các giao dịch chạy lần lượt theo một thứ tự nào đó. Đạt nó có giá: 2PL (khoá, dễ deadlock, chậm) hoặc SSI (serializable snapshot isolation — lạc quan, phát hiện xung đột lúc commit). Isolation yếu hơn serializable rẻ hơn nhưng để lọt anomaly — biết anomaly nào bị hở để quyết mức phù hợp.
Bài trước nói isolation "vỡ" khi phân tán. Bài này cụ thể hoá: vỡ thành những anomaly gì, và serializable nghĩa chính xác là gì. Đây là kiến thức để bạn không chỉ "bật transaction" mà biết mức isolation mình chọn thật sự chặn được cái gì.
1. Analogy — hai người cùng sửa một con số trên bảng trắng
Hai đồng nghiệp cùng cập nhật "số ghế còn trống = 1" trên một bảng trắng. Cả hai đọc "1", cả hai nghĩ "còn ghế, đặt được", cả hai ghi "0" và bán một vé. Bảng cuối cùng ghi "0" — trông đúng — nhưng đã bán hai vé cho một ghế. Không ai làm sai một mình; cái sai nằm ở chỗ họ đọc cùng lúc rồi ghi đè lên nhau (lost update), hoặc mỗi người quyết đúng theo cái mình thấy nhưng tổng hợp lại phá ràng buộc (write skew).
| Bảng trắng | Anomaly |
|---|---|
| Hai người cùng đọc "1" rồi cùng ghi "0" | Lost update |
| Mỗi người thấy "còn ghế" nên bán | Write skew (mỗi cái đúng, tổng sai) |
| Ghi làm đổi "còn mấy ghế" của người khác | Phantom |
| Bắt xếp hàng, mỗi lần một người | Serializable |
Anomaly = "ai cũng đúng phần mình mà tổng lại sai". Serializable = "bắt xếp hàng lần lượt" — chắc chắn đúng nhưng chậm hơn. Cả module isolation là tìm cách gần-serializable mà đỡ chậm.
flowchart TB A["Bac si A: doc 'B con truc' -> xin nghi"] --> W["Ca hai DOC truoc khi thay ghi cua nhau"] B["Bac si B: doc 'A con truc' -> xin nghi"] --> W W --> R["Ca hai duoc nghi -> KHONG con ai truc<br/>(write skew)"]
2. Ba anomaly quan trọng
Lost update. Hai giao dịch cùng làm read-modify-write trên một giá trị: đọc x, tính x+1, ghi. Nếu chúng đọc cùng giá trị cũ rồi ghi đè, một lần +1 biến mất (đếm view, tăng số dư). Chống bằng khoá đọc-để-ghi (SELECT ... FOR UPDATE), atomic increment, hoặc compare-and-set.
Write skew. Tổng quát hơn lost update: mỗi giao dịch đọc một tập dữ liệu, quyết định dựa trên đó, rồi ghi vào chỗ khác. Từng giao dịch giữ ràng buộc đúng theo cái nó đọc, nhưng chúng đọc trước khi thấy ghi của nhau, nên tổng hợp phá ràng buộc. Ví dụ kinh điển: quy định "luôn có ít nhất 1 bác sĩ trực"; hai bác sĩ đang trực cùng xin nghỉ, mỗi người kiểm "còn người kia trực" → cho nghỉ → không còn ai trực.
Phantom. Một giao dịch chạy truy vấn theo điều kiện (mọi phòng trống trong khung giờ X), quyết định dựa trên tập kết quả đó; một giao dịch khác chèn/xoá một hàng khớp điều kiện → làm tập kết quả đổi. Snapshot isolation (MVCC) chặn được phantom cho truy vấn read-only (ảnh chụp nhất quán không đổi giữa chừng); nhưng không chặn phantom kiểu đọc-rồi-ghi (read-then-write) — khi giao dịch đọc tập kết quả rồi tự ghi dựa trên đó. Đây chính là gốc của write skew.
3. Serializability là gì, và vì sao nó khó?
Snapshot isolation (mỗi giao dịch thấy một ảnh chụp nhất quán tại thời điểm bắt đầu) chặn được nhiều anomaly. Bạn đoán: nó có tự chặn write skew (hai bác sĩ cùng xin nghỉ) không? Vì sao có/không?
Câu trả lời là không — và lý do đi thẳng vào bản chất write skew. Snapshot isolation cho mỗi giao dịch đọc ảnh chụp của riêng nó, chụp tại lúc bắt đầu nên chưa thấy ghi của giao dịch kia. Cả hai bác sĩ đọc cùng một ảnh "người kia còn trực" rồi cùng cho mình nghỉ; không ai đọc phải ghi-dở của ai, nên snapshot thấy mọi thứ đều hợp lệ. Cái nó bỏ sót là quyết định của mỗi giao dịch dựa trên dữ liệu mà giao dịch kia sắp thay đổi — một mối phụ thuộc đọc-ghi mà snapshot không theo dõi. Đây là lý do nhiều DB gọi mức này là "serializable" mà vẫn để lọt write skew.
Vậy "serializable thật" là gì? Serializable là mức isolation mạnh nhất: đảm bảo kết quả của các giao dịch đồng thời y hệt như thể chúng chạy lần lượt theo một thứ tự tuần tự nào đó. Đạt được nó thì không phải lo bất kỳ anomaly nào — mọi thứ như single-threaded.
Ba cách đạt serializable thật:
- Thực thi tuần tự thật (một luồng, không đồng thời): đơn giản, đúng, nhưng không tận dụng nhiều core — chỉ hợp khi mỗi giao dịch rất nhanh.
- Two-phase locking (2PL): giao dịch giữ khoá trên mọi thứ nó đọc/ghi tới khi commit. Đảm bảo serializable nhưng chậm (giữ khoá lâu, giảm đồng thời) và dễ deadlock (hai giao dịch chờ khoá của nhau).
- Serializable snapshot isolation (SSI): lạc quan — cho các giao dịch chạy trên snapshot như bình thường, nhưng theo dõi liệu quyết định của chúng có dựa trên dữ liệu đã bị giao dịch khác thay đổi không; nếu có xung đột nguy hiểm, abort một giao dịch lúc commit. Nhanh khi ít xung đột, nhưng phải retry giao dịch bị abort.
4. Pitfall — tin "isolation level tên serializable" đã đủ, hoặc dùng mức yếu cho ràng buộc quan trọng
Nhiều database gọi mức isolation của chúng là "serializable" hoặc "repeatable read" nhưng thực chất là snapshot isolation — vẫn hở write skew.
❌ Dat isolation = "snapshot" (ten co the la 'repeatable read')
-> tuong da chong moi anomaly
-> write skew (2 bac si cung nghi) van xay ra -> pha rang buoc nghiep vu
❌ Chon mount isolation yeu (read committed) cho thao tac tinh tien
-> lost update: hai lan +tien doc cung so du -> mot lan mat
Hệ quả: ràng buộc nghiệp vụ quan trọng bị phá thỉnh thoảng (khi hai giao dịch đúng lúc đồng thời), rất khó tái hiện và debug.
Hướng đúng: biết chính xác mức isolation của DB bạn dùng thật sự chặn anomaly nào (đọc tài liệu, đừng tin cái tên). Với ràng buộc không được phép phá (đặt vé, số dư, "tối thiểu 1 trực"), dùng serializable thật (SSI hoặc 2PL, hoặc khoá tường minh FOR UPDATE) — chấp nhận chậm/retry. Với dữ liệu chịu được, dùng mức yếu hơn cho nhanh. Chọn mức theo anomaly bạn không được phép có, không theo mặc định.
5. 📚 Deep Dive
- Designing Data-Intensive Applications (Kleppmann) — Chương 7, mục "Weak Isolation Levels" và "Serializability" — lost update, write skew, phantom; và ba cách đạt serializable (actual serial execution, 2PL, SSI) cùng đánh đổi.
- Snapshot isolation vs serializable: Berenson et al. (1995), "A Critique of ANSI SQL Isolation Levels" — chỉ ra vì sao các tên isolation của ANSI gây hiểu nhầm, và write skew lọt qua snapshot isolation.
- SSI: Cahill, Röhm, Fekete (2008), "Serializable Isolation for Snapshot Databases" — cơ sở của serializable trong PostgreSQL.
Ghi chú: DDIA Ch7 nhấn: "serializable" là đảm bảo hành vi (như tuần tự), 2PL/SSI là cơ chế đạt nó. Đừng nhầm cái tên isolation level với đảm bảo thật.
6. Liên hệ các bài khác
- Bài 01 — Ôn ACID: isolation "vỡ" thành các anomaly cụ thể ở đây; serializable là mức mạnh nhất của isolation.
- Bài 03 — 2PC: 2PL (khoá cho isolation) khác 2PC (commit cho atomicity) — hai "two-phase" dễ nhầm, bài sau tách rõ.
- Module 2 — Multi-leader: write conflict ở multi-leader là họ hàng của lost update/write skew — cùng gốc "hai nơi quyết trên dữ liệu cũ".
- Module 6 — Linearizability: serializable (isolation, về giao dịch) khác linearizability (về một object) — hai đảm bảo dễ nhầm, phân biệt ở đó.
7. Tóm tắt
- Anomaly = cách giao dịch đồng thời phá nhau. Lost update: đọc-sửa-ghi chồng, mất một cập nhật. Write skew: mỗi giao dịch đúng theo cái nó đọc nhưng tổng phá ràng buộc. Phantom: ghi làm đổi tập kết quả truy vấn khác — snapshot isolation không chặn phantom kiểu write-skew (đọc-rồi-ghi).
- Serializability = kết quả y như chạy tuần tự — chuẩn vàng chống mọi anomaly.
- Snapshot isolation KHÔNG chặn write skew (mỗi giao dịch đọc ảnh chụp riêng) — dù nhiều DB gọi nó là "serializable".
- Đạt serializable thật: actual serial (một luồng), 2PL (khoá, chậm + deadlock), SSI (lạc quan, abort khi xung đột).
- Anti-pattern: tin cái tên "serializable" mà thực là snapshot (lọt write skew); hoặc dùng mức yếu cho ràng buộc quan trọng.
8. Tự kiểm tra
Q1Phân biệt lost update và write skew. Vì sao write skew tổng quát hơn?▸
Lost update: hai giao dịch làm read-modify-write trên cùng một giá trị; cùng đọc giá trị cũ rồi ghi đè → một cập nhật biến mất (vd hai lần +1 thành +1).
Write skew: mỗi giao dịch đọc một tập dữ liệu, quyết định dựa trên đó, rồi ghi vào chỗ khác. Từng cái giữ ràng buộc đúng theo cái nó đọc, nhưng chúng đọc trước khi thấy ghi của nhau nên tổng phá ràng buộc (2 bác sĩ cùng nghỉ vì mỗi người thấy "người kia còn trực").
Tổng quát hơn vì lost update là trường hợp đặc biệt (ghi vào cùng giá trị đã đọc); write skew cho phép đọc và ghi ở các chỗ khác nhau, nên khó phát hiện hơn và lọt qua nhiều mức isolation.
Q2Serializable nghĩa chính xác là gì? Vì sao đạt nó thì không cần lo bất kỳ anomaly nào?▸
Serializable đảm bảo kết quả của các giao dịch đồng thời y hệt như thể chúng chạy lần lượt theo một thứ tự tuần tự nào đó (không cần biết thứ tự nào, chỉ cần tồn tại một thứ tự tuần tự cho cùng kết quả).
Không cần lo anomaly nào vì mọi anomaly (lost update, write skew, phantom) đều là hiện tượng chỉ xuất hiện khi giao dịch xen kẽ. Nếu kết quả tương đương với một lịch chạy tuần tự — nơi không có xen kẽ — thì theo định nghĩa không hiện tượng xen kẽ nào tồn tại. Đó là lý do serializable là "chuẩn vàng": nó đưa suy luận về mức single-threaded, đơn giản nhất.
Q3Vì sao snapshot isolation KHÔNG chặn được write skew, dù nó chặn được nhiều anomaly khác?▸
Snapshot isolation cho mỗi giao dịch thấy một ảnh chụp nhất quán tại thời điểm bắt đầu — chưa thấy ghi của giao dịch đồng thời. Nó chặn tốt các anomaly kiểu "đọc thấy ghi dở của người khác".
Nhưng write skew là: cả hai giao dịch đọc (trên snapshot riêng, đều thấy "người kia còn trực"), rồi ghi vào chỗ khác nhau (mỗi người tự cho mình nghỉ). Không ai đọc ghi dở của ai — nên snapshot không thấy vấn đề. Cái sai là quyết định của mỗi giao dịch dựa trên dữ liệu mà giao dịch kia sắp thay đổi, và snapshot isolation không theo dõi mối phụ thuộc đó. Cần SSI (theo dõi read-write dependency) hoặc khoá tường minh mới chặn.
Q4So sánh 2PL và SSI về cách đạt serializable và cái giá của mỗi cách.▸
- 2PL (two-phase locking): bi quan — giao dịch giữ khoá trên mọi thứ nó đọc/ghi tới khi commit, nên không giao dịch nào đụng dữ liệu đang bị dùng. Giá: chậm (giữ khoá lâu → giảm đồng thời) và dễ deadlock (chờ khoá vòng tròn).
- SSI (serializable snapshot isolation): lạc quan — cho chạy trên snapshot bình thường, theo dõi read-write dependency, và abort giao dịch lúc commit nếu phát hiện xung đột nguy hiểm. Giá: phải retry giao dịch bị abort; hiệu năng tốt khi ít xung đột, xấu khi tranh chấp cao.
Chọn: 2PL cho workload tranh chấp cao/ổn định; SSI cho workload đọc-nhiều ít xung đột (PostgreSQL dùng SSI cho mức Serializable).
Q5Một DB có mức isolation tên 'Repeatable Read' thực chất là snapshot isolation. Bạn cần đảm bảo 'luôn có tối thiểu 1 nhân viên trực'. Dùng mức đó có an toàn không, và nên làm gì?▸
Không an toàn. "Tối thiểu 1 người trực" là ràng buộc kiểu write skew: hai nhân viên cùng xin nghỉ, mỗi người kiểm (trên snapshot) thấy "còn người kia trực" → cùng được nghỉ → phá ràng buộc. Snapshot isolation (dù tên là "Repeatable Read") không chặn write skew.
Nên làm: dùng serializable thật (mức Serializable/SSI của DB) cho giao dịch này, hoặc khoá tường minh (khoá dòng "ca trực" bằng SELECT ... FOR UPDATE để hai giao dịch không cùng quyết trên trạng thái cũ). Nguyên tắc: chọn mức theo anomaly không được phép có, và đừng tin cái tên isolation level — đọc tài liệu xem nó thật sự chặn gì.
Bài tiếp theo: Two-phase commit (2PC) — và vì sao nó blocking, đắt
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên