TechDogy

(paduvi)

You can do anything, but not everything..

Transaction Isolation (Part 1): Concurrency Control Problem

Categories:,

Ở trong bài viết Bạn đã hiểu đúng về Transaction chưa?, tôi có giới thiệu qua về ACID. Trong số 4 từ khóa: A (Atomicity), C (Consistency), I (Isolation) và D (Durability), thì có lẽ Isolation là thành phần được mọi người quan tâm nhiều nhất, đặc biệt là những đối tượng sau nên đọc bài viết này:

  • Những anh em dev cần rà soát lại xem mình đã sử dụng transaction đúng cách chưa?
  • Các tech lead đang phân vân lựa chọn Database mình sẽ sử dụng cho dự án sắp tới.
  • Người muốn tìm hiểu sâu về công nghệ bên dưới của các Database, tránh việc bị nhà phát triển chạy marketing tẩy trắng bằng những miếng bánh vẽ vô cùng béo bở.

Phần 1 ta sẽ làm quen với…

Những tình huống Race condition hay gặp trong Database:

Dirty Write

Điều gì sẽ xảy ra khi 2 transaction cùng tìm cách update lại giá trị của 1 bản ghi trong Database? Giả thiết đơn giản nhất đó là yêu cầu Write phía sau sẽ ghi đè lên yêu cầu xảy ra trước đó.

Tuy nhiên, ta sẽ bắt gặp tình huống mà transaction xảy ra trước chưa kịp commit, thì cái tới sau đã ghi đè lên, dẫn tới việc kết quả bị xáo trộn, “râu ông này cắm cằm bà kia”, thiếu nhất quán giống như minh họa bên dưới đây:

Ở ví dụ này: 2 khách hàng tên là Alice và Bob cùng đặt mua online 1 chiếc xe ô tô, Alice nhanh tay hơn và đặt lệnh trước, tuy nhiên kết quả cuối cùng lại là ô tô thuộc về Bob mặc dù hóa đơn vẫn đứng tên Alice.

Như vậy, chúng ta có thể thấy rằng: cách giải quyết đơn giản nhất đó là khi cần write vào bản ghi, ta sẽ acquire lock của row đó trước đã. Khi nào transaction kết thúc thì mới release lock.

Một Isolation Level được coi là “no dirty write” khi mà Database chỉ write lên những bản ghi đã được commit trước đó. Nếu 2 transaction cùng muốn ghi đồng thời, thì cái nào tới sau sẽ phải chờ đợi transaction trước đó commit success hoặc rollback.

Dirty Read

Ngược với Dirty Write, đây là tình trạng mà Database read ra giá trị chưa được commit.

Việc đảm bảo “no dirty read” giúp cho người dùng cuối tránh được những trường hợp khó hiểu như là:

  • Có thông báo mới nhưng số badge thì vẫn chưa nhảy +1
  • Lấy ra dữ liệu đã bị rollback.

Tuy nhiên “tránh vỏ dưa phải vỏ dừa”, no-dirty read Isolation lại tiềm ẩn những lỗi dưới đây:

Read Skew (Non-repeatable Read)

Giả sử Alice có 2 tài khoản ngân hàng trong cùng 1 cái ví điện tử (VinID hay Momo blah blah), trong mỗi tài khoản hiện đang chứa 500$.

Alice bắn 100$ từ tài khoản 2 sang tài khoản 1. Trong quá trình giao dịch đang xử lý, Alice dùng một chiếc điện thoại khác của mình để mở app lên kiểm tra. Trên giao diện hiển thị tài khoản 1 có 500$ trong khi tài khoản 2 đã giảm chỉ còn 400$.

Lỗi này thường xuất hiện đối với những read-only transaction, nó vẫn đảm bảo eventually consistency miễn là user reload lại app ngay sau đó (hay còn gọi là repeated read). Vì chỉ gây ảnh hưởng tới kết quả read nên nó không được xếp vào loại lỗi nghiêm trọng.

Tuy nhiên, trong một số trường hợp như sau thì ta cần đòi hỏi kết quả phải 100% consistency vào thời điểm lệnh được gọi ra:

  • Truy vấn thống kê trên 1 đoạn dữ liệu lớn: thời điểm bắt đầu truy vấn cách thời điểm kết thúc lên tới vài giây, thậm chí tận vài phút.
  • Backup dữ liệu: có thể kéo dài lên tới hàng tiếng đồng hồ. Chắc chắn chúng ta sẽ không muốn dữ liệu sao lưu sẽ bị lẫn lộn giữa cả version cũ và version mới.

Lost Update

Lỗi này xuất hiện khi read ra 1 bản ghi, modify nó ở phía chương trình rồi lưu trở lại vào Database (read-modify-write cycle). Ví dụ minh họa:

Lỗi này được xếp vào hàng nghiêm trọng vì có thể gây sai sót logic trong quá trình ghi vào Database. Có nhiều cách để giải quyết nó, trong số đó bao gồm việc bổ sung lock cho cả read operation. Tuy nhiên, cách này không giải quyết được triệt để, vì nó chỉ đảm bảo transaction cho 1 row, chứ không đảm bảo được trên nhiều row.

Nhớ lại trong bài Bạn đã hiểu đúng về Transaction chưa?, tôi có nhắc tới việc MongoDB từng quảng cáo rằng có ACID, dù nó chỉ hỗ trợ transaction trên một single document mà thôi (trước version 4.0).

Phantoms và Write Skew

Đây chính là phiên bản nâng cấp của lỗi Lost Update: thường xảy ra khi truy vấn search rồi check 1 số điều kiện và save lại vào Database (không cùng modify 1 object nên không bị lock write). Kết quả của câu lệnh write sẽ làm thay đổi kết quả trả về của truy vấn search trước đó.

Giải thích cho trường hợp của hình vẽ minh họa bên trên: Trong một bệnh viện, luôn phải có tối thiểu 1 bác sĩ trực. Bác sĩ Alice và Bob cùng gửi yêu cầu xin về nhà vào cùng thời điểm vì buồn ngủ/mệt/lý do gia đình. Số lượng bác sĩ đang trực là 2 (thỏa mãn lớn hơn 1), nên yêu cầu của cả 2 đều được chấp thuận. Kết quả là không còn bác sĩ nào trực ở bệnh viện cả.

Ngoài ra, ta có thể gặp rất nhiều trường hợp tương tự khác như:

  • Ứng dụng đặt phòng
  • Đăng ký tên đăng nhập (username phải là duy nhất chưa từng tồn tại).

Vấn đề bên trên được gọi là Write Skew, motip chung của loại lỗi này đều gồm 3 bước:

  1. Thực hiện truy vấn search trên 1 điều kiện nào đó (query by predicate – không phải get by id). Ví dụ: Đếm tổng số bác sĩ đang trực.
  2. Kiểm tra kết quả thu được có thỏa mãn điều kiện nào đó không? Ví dụ: Tổng số bác sĩ đang trực lớn hơn 1.
  3. Thực hiện câu lệnh INSERT/UPDATE/DELETE làm thay đổi kết quả của câu truy vấn ở bước 1. Ví dụ: Cho bác sĩ Alice ra về.

Tổng quát hơn, lỗi do operation write ở 1 transaction khác làm thay đổi kết quả query của transaction hiện tại được gọi là Phantoms. Trong đó, Write Skew là 1 trường hợp cụ thể của Phantoms. Ngoài ra, tương tự còn có Phantom Read hay xuất hiện ở read-only transaction, dù lỗi này không phổ biến cho lắm:

  1. Transaction A thực hiện truy vấn search trên 1 điều kiện nào đó (không phải get 1 key cụ thể). Ví dụ: Đếm tổng số bác sĩ đang trực (=2).
  2. Cùng lúc đó, transaction B thực hiện câu lệnh INSERT/UPDATE/DELETE làm thay đổi kết quả của câu truy vấn ở bước 1. Ví dụ: Cho bác sĩ Alice ra về.
  3. Truy vấn lại điều kiện search ban đầu ở transaction A cho ra kết quả khác với lần thứ nhất. Ví dụ: Đếm tổng số bác sĩ đang trực (=1).

Giải pháp chung cho 2 vấn đề này ta có thể sử dụng range-lock thay vì lock từng bản ghi, đây chính là lợi thế giúp cho hầu hết các cơ sở dữ liệu quan hệ (đa phần đều sử dụng B-Trees index) thường là sự lựa chọn tốt nhất cho những ứng dụng cần transaction chặt chẽ. Tôi có từng nhắc tới range-lock ở trong bài Database 201: B-Trees, anh em nào chưa biết thì có thể xem 😀

Phần tiếp theo: Transaction Isolation (Part 2): Isolation Level

Latest Comments:

  1. Mình đi search về storage engine thì thấy series về bài của bạn (mình đoán là bạn cũng đọc từ…

  2. Series rất hay, ủng hộ admin làm thêm về các database khác như Scylladb (discord mới migrate từ Cassandra sang)

  3. bài viết rất chất lượng, ủng hộ mạnh tác giả

  4. Mình đang làm về authentication thì phải tìm hiểu thêm về JWE (Json Web Encryption) và JWS (Json Web Signature).…

Leave a Reply

Your email address will not be published. Required fields are marked *