Serializable Isolation — 2PL và SSI cho Distributed Systems (DDIA)

Phong

Mở đầu

Ảnh: Brett Sayles — Pexels

Trong các phần trước của Chương 7, chúng ta đã thấy các weak isolation levels như Read Committed, Snapshot Isolation, và Repeatable Read đều có những concurrency bugs riêng — dirty writes, lost updates, write skew, phantom reads. Những isolation level yếu hơn cho phép transaction chạy song song với ít blocking hơn, nhưng đánh đổi bằng sự đảm bảo về tính nhất quán của dữ liệu.

Vậy nếu bạn cần sự đảm bảo mạnh nhất — rằng kết quả của các transaction chạy đồng thời tương đương với việc chúng chạy tuần tự theo một thứ tự nào đó — thì bạn cần Serializable Isolation. Đây là isolation level mạnh nhất trong ACID, và DDIA giới thiệu hai cơ chế chính để đạt được nó: Two-Phase Locking (2PL) và Serializable Snapshot Isolation (SSI).

Two-Phase Locking (2PL) — Cách tiếp cận pessimistic

2PL là một trong những phương pháp lâu đời nhất để đạt serializability. Ý tưởng cốt lõi rất đơn giản: các transaction không được phép đọc hoặc ghi dữ liệu nếu chưa có "quyền" — tức là phải xin lock trước.

2PL hoạt động qua hai phase rõ rệt:

  • Phase 1 — Expanding (Growing): Transaction xin các lock cần thiết (shared lock cho đọc, exclusive lock cho ghi). Có thể xin thêm lock nhưng không được release lock nào.
  • Phase 2 — Shrinking (Contracting): Sau khi đã có tất cả lock cần thiết, transaction bắt đầu release lock. Không được xin thêm lock mới.

Điểm quan trọng: nếu transaction A đang giữ exclusive lock trên một row, transaction B muốn đọc hoặc ghi row đó sẽ phải đợi — đây là lý do 2PL có performance overhead đáng kể. Deadlock cũng là vấn đề thường gặp: transaction A lock row 1, cần row 2; B lock row 2, cần row 1. Database sẽ phát hiện và abort một trong hai.

Phiên bản nâng cấp là Strong Strict 2PL (SS2PL) — release exclusive lock chỉ sau khi transaction commit hoặc abort. Đây là biến thể phổ biến trong thực tế vì nó tránh được cascading aborts.

Predicate Locks và Index-Range Locks

Một vấn đề với 2PL là phantom reads — khi transaction đọc một tập hợp row theo điều kiện, và transaction khác insert row mới thoả điều kiện đó. Row-level lock không đủ để ngăn chặn phantom, vì row mới chưa tồn tại để khoá.

Ảnh: Brett Sayles — Pexels

Giải pháp là predicate lock — khoá không phải trên một row cụ thể, mà trên một điều kiện (predicate). Ví dụ: "khoá tất cả các phòng có status = 'booked' trong ngày 2024-01-15". Nếu transaction khác muốn insert phòng mới với cùng điều kiện, nó sẽ bị chặn.

Trong thực tế, predicate lock quá tốn kém nên hầu hết database dùng index-range locking (next-key locking) — khoá một khoảng trong index thay vì khoá predicate. Nếu index phù hợp, nó approximates predicate lock tốt hơn row-level lock, và được dùng trong MySQL/InnoDB, PostgreSQL (đối với Serializable mode dùng SSI, không phải 2PL).

Serializable Snapshot Isolation (SSI) — Cách tiếp cận optimistic

2PL có một nhược điểm lớn: nó pessimistic — luôn cho rằng xung đột sẽ xảy ra và khoá trước. Điều này dẫn đến nhiều contention và giảm throughput, đặc biệt với workload có ít xung đột.

Serializable Snapshot Isolation (SSI) là một phát minh tương đối gần (2008, bởi Michael Cahill và cộng sự), được thiết kế để kết hợp ưu điểm của Snapshot Isolation (read không bao giờ block) với serializability. SSI là cơ chế mặc định cho Serializable isolation trong PostgreSQL kể từ phiên bản 9.1.

SSI hoạt động dựa trên nguyên lý optimistic concurrency control:

  • Các transaction đọc dữ liệu từ snapshot (không block)
  • Khi ghi, database theo dõi các dependency giữa các transaction
  • Trước khi commit, database kiểm tra xem có cyclic dependency (vòng tròn) giữa các transaction không — nếu có, một transaction sẽ bị abort
Ảnh: Divinetechygirl — Pexels

Có hai tình huống nguy hiểm mà SSI phát hiện:

  1. SIREAD lock phát hiện xung đột đọc-sau-ghi muộn: Khi transaction T1 đọc dữ liệu và T2 ghi vào dữ liệu đó sau, database đặt một "SIREAD lock" (semi-materialized) để theo dõi — không block, chỉ ghi nhận. Nếu sau đó T1 ghi vào dữ liệu mà T2 đã đọc, tạo thành vòng tròn dependency.
  2. Phát hiện xung đột ghi-sau-đọc: Khi transaction T1 đọc dữ liệu, và T2 ghi đè dữ liệu đó, rồi T1 commit — database kiểm tra xem có out-of-date premise không.

So sánh 2PL và SSI

Cả hai đều đạt serializability, nhưng có trade-offs khác nhau:

  • Performance: 2PL blocking nhiều → throughput thấp khi contention cao. SSI optimistic → performance tốt hơn khi contention thấp, nhưng abort rate tăng khi contention cao.
  • Read performance: 2PL có thể block read nếu write lock đang giữ. SSI read không bao giờ block.
  • Deadlock: 2PL dễ bị deadlock (cần database phát hiện + abort). SSI không có deadlock, nhưng abort do serialization failure.
  • Implementations: 2PL dùng nhiều trong commercial databases (DB2, SQL Server). SSI là tiêu chuẩn trong PostgreSQL.

Key Takeaways

  • Serializable isolation đảm bảo kết quả transaction tương đương với chạy tuần tự — mạnh nhất trong ACID
  • 2PL dùng lock pessimistic — đơn giản nhưng performance overhead cao do blocking và deadlock
  • SSI dùng optimistic detection — read không block, phát hiện xung đột trước commit, abort transaction nếu cần
  • Predicate lock và index-range lock giải quyết phantom reads trong 2PL
  • Chọn 2PL hay SSI tuỳ workload: contention cao → 2PL/SS2PL có thể predictable hơn; contention thấp → SSI cho performance tốt hơn
  • PostgreSQL dùng SSI từ 9.1, MySQL/InnoDB dùng next-key locking trong REPEATABLE READ

Kết

Chọn isolation level không chỉ là chuyện "mặc định là gì" — nó ảnh hưởng trực tiếp đến tính đúng đắn và hiệu năng của hệ thống. Serializable isolation cho bạn sự an tâm về mặt dữ liệu, nhưng phải trả giá bằng performance. Hiểu được 2PL và SSI giúp bạn đưa ra quyết định sáng suốt hơn khi thiết kế hệ thống.

Trong bài tiếp theo, chúng ta sẽ tìm hiểu về Distributed Transactions và các phương pháp đồng bộ dữ liệu giữa các node.