Transaction concurrency

Transaction Isolation 101: Concurrency Control Problem

29 tháng 12, 2020
8 phút đọc
Transaction Isolation 101: Concurrency Control Problem

Trong bài viết "Bạn đã hiểu đúng về Transaction chưa?", tôi đã giới thiệu qua về ACID. Trong số 4 từ khóa, có lẽ Isolation (Sự cô lập) là thành phần phức tạp và được quan tâm nhiều nhất.

Khi nhiều transaction chạy đồng thời, nếu mức độ cô lập không đủ mạnh, chúng ta sẽ gặp phải những tình huống race condition khó lường, có thể dẫn đến dữ liệu bị hỏng hoặc vi phạm nghiêm trọng các quy tắc nghiệp vụ.

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ở.

Bài viết này sẽ điểm mặt đặt tên những "kẻ phá hoại" kinh điển đó. Phần giải pháp để ngăn chặn chúng sẽ được phân tích kỹ trong bài kế tiếp.


Dirty Write – "Râu ông nọ cắm cằm bà kia"#

Đây là tình huống hỗn loạn nhất. Hai transaction cùng cố gắng ghi đè (update) vào một bản ghi. Vấn đề xảy ra khi transaction sau ghi đè lên thay đổi của transaction trước, trong khi transaction trước chưa kịp commit hoặc có thể bị rollback. Kết quả là dữ liệu trở thành một mớ hỗn độn không thể lường trước.

Ví dụ kinh điển:
Hai khách hàng Alice và Bob cùng đặt mua chiếc xe ô tô cuối cùng. Giao dịch của họ diễn ra song song.

  • Alice (nhanh tay hơn): Cập nhật listingsinvoices thành tên mình.

  • Bob (chậm hơn một chút nhưng chưa commit sau): Cũng cập nhật listingsinvoices thành tên mình.

-- Transaction của Alice
BEGIN;
UPDATE listings SET buyer = 'Alice' WHERE id = 1234;
UPDATE invoices SET recipient = 'Alice' WHERE listing_id = 1234;
COMMIT;

-- Transaction của Bob (chạy song song, commit sau)
BEGIN;
UPDATE listings SET buyer = 'Bob' WHERE id = 1234;
UPDATE invoices SET recipient = 'Bob' WHERE listing_id = 1234;
COMMIT;

Kết quả tồi tệ có thể xảy ra: Hệ thống ghi nhận Bob là chủ sở hữu chiếc xe, nhưng hóa đơn lại gửi cho Alice. Dữ liệu đã hoàn toàn mất tính nhất quán.


Dirty Read – Đọc còn ten bẩn chưa được commit#

Nếu Dirty Write là gây hỗn độn khi ghi, thì Dirty Read là gây nhầm lẫn khi đọc. Nó xảy ra khi một transaction đọc được giá trị từ một transaction khác chưa được commit. Nếu transaction kia sau đó bị rollback, giá trị mà transaction đầu đọc được coi như “dirty” – nó chưa bao giờ thực sự tồn tại..

Ví dụ kinh điển:

  • Bạn mở app và thấy một thông báo tin nhắn mới, nhưng số badge đếm tin nhắn trên icon lại không tăng.

  • Bạn đọc được một thông tin quan trọng, nhưng sau vài giây nó biến mất không dấu vết.

Đó chính là hậu quả của Dirty Read.


Read Skew (Non-repeatable Read)#

Lỗi này chủ yếu ảnh hưởng đến trải nghiệm đọc dữ liệu. Nó xảy ra khi một transaction đọc cùng một dữ liệu hai lần, nhưng giữa hai lần đọc đó, một transaction khác commit và thay đổi dữ liệu ấy, dẫn đến hai kết quả khác nhau được trả về trong cùng một transaction.

Ví dụ thực tế – "Momo kỳ quặc"#

Alice có 2 tài khoản trong ví Momo, mỗi tài khoản $500. Cô chuyển $100 từ tài khoản 2 sang tài khoản 1.

  • Trong lúc giao dịch đang xử lý, Alice mở app trên điện thoại khác để kiểm tra.

    • Màn hình hiển thị: Tài khoản 1: $500, Tài khoản 2: $400.

    • Tổng số tiền hiển thị là $900, tạo ra một trạng thái không nhất quán tạm thời.

  • Sau khi giao dịch commit, nếu app reload, mọi thứ sẽ nhất quán: Tài khoản 1: $600, Tài khoản 2: $400.

Tác hại:

  • Mặc dù không làm hỏng dữ liệu cuối cùng, Read Skew có thể gây nhầm lẫn và mất niềm tin cho người dùng.

  • Tuy nhiên, trong một số trường hợp bắt buộc, kết quả đòi hỏi 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 – Cập nhật "bốc hơi"#

Lỗi này tinh vi hơn và xảy ra trong kiểu mẫu Đọc - Sửa - Ghi (Read-Modify-Write). Hai transaction cùng đọc một giá trị cũ, mỗi transaction thay đổi giá trị đó dựa trên logic riêng, rồi ghi đè kết quả lên nhau. Kết quả, thay đổi của transaction commit trước bị "thất lạc", như chưa từng xảy ra.

Ví dụ đơn giản:
Hai người dùng cùng click like ở thời điểm bộ đếm lượt like (counter = 42).

  1. Cả hai cùng đọc giá trị 42.

  2. Cả hai cùng tính toán 42 + 1 = 43.

  3. Lần lượt ghi giá trị 43 vào database.

Kết quả cuối cùng: Bộ đếm là 43, trong về mặt logic, nó phải là 44. Một lượt like đã "bốc hơi".


Phantom Read & Write Skew – Ảo giác#

Đây là cặp bài trùng tinh vi và nguy hiểm nhất, thường phá vỡ các ràng buộc nghiệp vụ (business constraint).

  • Phantom Read: Xảy ra khi một transaction thêm (INSERT) hoặc xóa (DELETE) các bản ghi, làm thay đổi kết quả của một truy vấn (query) đã được thực thi trước đó trong một transaction khác. Giống như có "bóng ma" xuất hiện hoặc biến mất giữa chừng.

  • Write Skew: Là trường hợp đặc biệt của Phantom Read, nhưng phức tạp hơn. Hai transaction cùng đọc một tập hợp dữ liệu, dựa trên kết quả đó để đưa ra quyết định ghi độc lập lên các bản ghi khác nhau, và hành động ghi này của mỗi bên lại làm vô hiệu hóa điều kiện ban đầu mà cả hai cùng dựa vào.

Ví dụ kinh điển về Write Skew – "Bệnh viện không bác sĩ trực"#

Quy tắc: Luôn phải có ít nhất 1 bác sĩ trực trong ca.
Hiện tại có 2 bác sĩ Alice và Bob đang trực. Cả hai cùng gửi yêu cầu nghỉ cùng lúc.

-- Transaction của Alice
BEGIN;
-- Đếm số bác sĩ đang trực: Kết quả = 2 (thỏa mãn >=1)
SELECT COUNT(*) FROM doctors WHERE on_call = true AND shift_id = 1234;
-- Alice quyết định nghỉ
UPDATE doctors SET on_call = false WHERE name = 'Alice' AND shift_id = 1234;
COMMIT;

-- Transaction của Bob (chạy song song)
BEGIN;
-- Cũng đếm số bác sĩ đang trực: Kết quả vẫn = 2 (vì Alice chưa commit)
SELECT COUNT(*) FROM doctors WHERE on_call = true AND shift_id = 1234;
-- Bob cũng quyết định nghỉ
UPDATE doctors SET on_call = false WHERE name = 'Bob' AND shift_id = 1234;
COMMIT;
  1. Transaction của Alice kiểm tra: "Có 2 bác sĩ trực" → Điều kiện ">=1" thỏa mãn → Cho Alice nghỉ.

  2. Transaction của Bob song song cũng kiểm tra: "Vẫn thấy 2 bác sĩ trực" (vì Alice chưa commit) → Điều kiện ">=1" thỏa mãn → Cho Bob nghỉ.

Kết quả thảm họa: Cả hai đều được nghỉ, 0 bác sĩ trực. Quy tắc nghiệp vụ cốt lõi đã bị phá vỡ hoàn toàn.

Các ví dụ thực tế khác của Write Skew bạn có thể gặp#

  • Ứng dụng đặt phòng: Chỉ còn 1 phòng trống, nhưng 2 người cùng đặt thành công.

  • Đăng ký tên người dùng: Kiểm tra "tên chưa tồn tại" xong, 2 request cùng tạo tài khoản với một tên giống nhau thành công.


Tóm tắt các vấn đề#

LỗiBản chấtHậu quả chính
Dirty WriteGhi đè lên thay đổi chưa commit.Dữ liệu hỗn độn, mất nhất quán nghiêm trọng.
Dirty ReadĐọc giá trị "rác" chưa commit.Thông tin hiển thị sai, gây hiểu lầm.
Lost UpdateMất thay đổi trong chu kỳ Đọc-Sửa-Ghi.Dữ liệu bị sai lệch (ví dụ: đếm thiếu).
Phantom/Write SkewThay đổi dữ liệu làm vô hiệu điều kiện truy vấn trước đó.Phá vỡ ràng buộc nghiệp vụ (quan trọng nhất).
Read SkewHai lần đọc trong cùng giao dịch ra giá trị khác nhau.Trải nghiệm người dùng kém, thấy dữ liệu tạm thời không nhất quán.

Như vậy, từ những hỗn độn cơ bản như Dirty Write cho đến những vấn đề phức tạp, khó phát hiện như Write Skew, thế giới của Transaction Isolation ẩn chứa nhiều cạm bẫy.

Vậy làm thế nào để chống lại chúng? Câu trả lời nằm ở việc lựa chọn mức Isolation (cô lập) phù hợp và các kỹ thuật khóa (locking) hiệu quả. Chúng ta sẽ cùng phân tích kỹ các giải pháp này trong bài viết tiếp theo: "Transaction Isolation 102: Isolation Level".

Hãy cùng đón đọc!

Khám phá thêm