SQL: Thiết kế dễ gây ra lỗi nghiêm trọng về đồng thời
Thiết kế của SQL và các hệ thống cơ sở dữ liệu quan hệ khiến việc vô tình tạo ra các lỗi đồng thời trở nên quá dễ dàng. Bài viết phân tích một ví dụ kinh điển về chuyển tiền, chỉ ra các vấn đề về tính nguyên tử, lỗi TOCTOU và deadlock, đồng thời kêu gọi cần có những công cụ tốt hơn đảm bảo tính đúng đắn như cách tiếp cận của Rust.
Thiết kế của SQL và các hệ thống cơ sở dữ liệu quan hệ khiến việc vô tình tạo ra các lỗi đồng thời (concurrency bugs) trở nên quá dễ dàng. Dưới đây là một ví dụ trong sách giáo khoa về thủ tục chuyển tiền sử dụng TSQL; Alice muốn gửi mười đô la cho Bob, và để ngăn Alice bị thấu chi tài khoản, chúng ta trước tiên kiểm tra xem cô ấy có đủ tiền hay không.
Đoạn mã này trông có vẻ hoàn toàn hợp lý, nhưng nó chứa một số lỗi nghiêm trọng. Bạn có thể phát hiện ra chúng không?
Vấn đề về tính nguyên tử
Đầu tiên, nếu thủ tục này bị hủy giữa chừng, chúng ta có thể đã trừ tiền từ tài khoản của Alice mà chưa chuyển bất kỳ khoản nào cho Bob. Alice sẽ không hài lòng về điều này, và chúng ta đã làm mất tiền trong quá trình này. Chúng ta muốn tất cả các lần chuyển tiền đều thành công, hoặc không có lần nào cả; giải pháp là bọc thủ tục trong một giao dịch (transaction):
BEGIN TRANSACTION;
-- Các bước chuyển tiền
COMMIT;
Vậy đã xong chưa? Chưa hẳn.
Lỗi TOCTOU (Time-of-check to time-of-use)
Giả sử Alice thực hiện hai lần chuyển tiền song song cho Bob, gọi là T1 và T2. Hãy xem xét điều gì sẽ xảy ra:
T2 kiểm tra số dư trước khi T1 rút bất kỳ khoản tiền nào từ tài khoản của Alice — vì vậy khi T2 cuối cùng thực hiện việc rút tiền, tài khoản có thể bị thấu chi. Đây là lỗi TOCTOU (Thời điểm kiểm tra đến thời điểm sử dụng): Điều kiện tiên quyết thay đổi giữa lúc chúng ta kiểm tra nó và lúc chúng ta thực hiện hành động dựa trên nó.
Giải pháp là khóa tài khoản của Alice cho đến khi giao dịch hoàn tất. Chúng ta có thể thay đổi mức độ cô lập (isolation level) để khóa được tự động thu nhận, hoặc khóa hàng tài khoản thủ công:
SELECT balance FROM Accounts WITH (UPDLOCK) WHERE name = 'Alice';
Gợi ý UPDLOCK sẽ lấy một khóa cấp hàng (row-level lock) trên tài khoản của Alice khi lệnh SELECT chạy; các giao dịch khác muốn sửa đổi tài khoản của Alice sẽ bị chặn cho đến khi khóa được giải phóng.
Deadlock và giải pháp
Vậy nếu Alice và Bob đều cố gắng chuyển tiền cho nhau cùng một lúc thì sao? Hãy lập lại các giao dịch một lần nữa:
T1 chờ khóa của T2 trên Bob; T2 chờ khóa của T1 trên Alice — chúng ta đã rơi vào tình trạng bế tắc (deadlock). Giải pháp là thu nhận tất cả các khóa ngay từ đầu:
-- Khóa Alice trước, sau đó mới khóa Bob
SELECT balance FROM Accounts WITH (UPDLOCK) WHERE name = 'Alice';
SELECT balance FROM Accounts WITH (UPDLOCK) WHERE name = 'Bob';
Chúng ta đã sửa các lỗi đồng thời trong mã gốc, nhưng trong quá trình đó, mã dài thêm khoảng 50% và trở nên khó đọc hơn. Chắc chắn, bạn có thể lập luận rằng có những cách khác "đúng chuẩn" hơn để sửa mã này, nhưng điểm mấu chốt vẫn còn đó: Một chương trình SQL trông có vẻ hoàn toàn hợp lý có thể đầy rẫy những lỗi nghiêm trọng.
Nếu bạn đang xây dựng một trang mạng xã hội, việc một người dùng thích một bài viết hai lần có thể không phải là vấn đề lớn, nhưng nếu hệ thống không ghi nhận rằng một bệnh nhân đã được dùng thuốc, điều đó có thể gây ra hậu quả chết người. Đối với các hệ thống mà tính đúng đắn là quan trọng, chúng ta cần những công cụ tốt hơn.
Tôi muốn một giải pháp thay thế cho SQL áp dụng cách tiếp cận "đồng thời an toàn" (fearless concurrency) của Rust — tức là, làm cho hành vi đúng trở thành mặc định, và cung cấp các "lối thoát không an toàn" (unsafe) nếu cần thiết. Một số đề xuất cụ thể:
Hệ thống này sẽ đi kèm với các sự đánh đổi khác; ví dụ, nó có thể có thông lượng thấp hơn các hệ thống SQL hiện đại. Nhưng điều đó không sao — chúng ta vẫn có SQL cho các trường hợp sử dụng mà tính đúng đắn ít quan trọng hơn.
Người đọc tinh ý có thể nhận thấy tôi không bao gồm mệnh đề
ORDER BYkhi thu nhận các khóa hàng. Bạn có thể nghĩORDER BYlà cần thiết để lấy khóa theo đúng thứ tự, nhưng đây là một "sự thật thú vị": Khóa thường được thu nhận theo thứ tự các hàng được đọc bởi cơ sở dữ liệu, chứ không phải theo thứ tự chúng xuất hiện trong kết quả. Điều này có nghĩa là việc ngăn chặn tất cả các deadlock có thể không thực tế, hoặc thậm chí không thể.
Bài viết liên quan

Phần mềm
Plugin Checkmarx Jenkins bị xâm phạm trong cuộc tấn công chuỗi cung ứng
11 tháng 5, 2026

Công nghệ
Substrate (YC S24) tuyển dụng Technical Success Manager cho nền tảng AI chuyên xử lý thanh toán y tế
13 tháng 5, 2026

Phần mềm
Bun công bố hướng dẫn chuyển đổi sang Rust, nhưng gọi dự án viết lại là "chưa chín muồi"
05 tháng 5, 2026
