Câu 1Tên của thuộc tính coi hai UPDATE (trừ + cộng) của một chuyển khoản như một đơn vị là gì — cùng commit nếu cả hai thành công, hoặc cùng hoàn tác nếu có gì sai ở giữa?
Transaction và khóa
Đi qua tính nguyên tử với BEGIN/COMMIT/ROLLBACK, rollback một phần với SAVEPOINT, AUTOCOMMIT, và các khái niệm về khóa, mức cô lập, và các thuộc tính ACID — tất cả với sơ đồ và ví dụ code chỉ đọc.
Bài này được xây dựng từ sơ đồ và code chỉ đọc
Transaction chỉ thực sự sống động trên một RDBMS thật khi nhiều người dùng truy cập cơ sở dữ liệu cùng lúc, và console trong trình duyệt của khóa học này không thể tái hiện trung thực hành vi đó.
Đó là lý do bài này không có bài tập chạy được — hãy đọc các khối code trong từng phần như ví dụ để chạy trên một RDBMS thật (MySQL / PostgreSQL / Oracle, v.v.).
Transaction là gì — coi nhiều câu lệnh SQL như một đơn vị
Một transaction là cách coi nhiều câu lệnh SQL như một đơn vị duy nhất: nếu tất cả đều thành công, chúng được commit cùng nhau; nếu có gì sai ở giữa, chúng được rollback cùng nhau.
Điều này thiết yếu cho bất kỳ quy trình nào mà việc chỉ một nửa công việc thành công sẽ là vấn đề — nghĩ đến chuyển khoản ngân hàng (một UPDATE để trừ, một để cộng), hoặc xác nhận đơn hàng online (giảm tồn kho, chèn dòng đơn hàng).
Dạng cơ bản là BEGIN; (bắt đầu) → vài câu lệnh → COMMIT; (xác nhận) / ROLLBACK; (hoàn tác).
Các khối code trong bài này dùng bảng account đại diện cho tài khoản ngân hàng. Ba cột là account_id, owner (tên chủ tài khoản), và balance. Giả sử dữ liệu ban đầu sau khi bạn đọc theo.
| account_id | owner | balance |
|---|---|---|
| 1 | Alice | 1000 |
| 2 | Bob | 500 |
| 3 | Carol | 800 |
| 4 | Dave | 1200 |
Tính nguyên tử — BEGIN ... COMMIT / ROLLBACK
Khi bạn BEGIN; rồi COMMIT;, mọi thay đổi giữa hai câu lệnh đó được áp dụng cùng nhau.
Chạy ROLLBACK; thay thế và mọi thay đổi sau BEGIN; được hoàn tác, đưa cơ sở dữ liệu về trạng thái trước khi transaction bắt đầu.
Tính chất "hoặc tất cả hoặc không gì" này được gọi là tính nguyên tử — nghĩa là một đơn vị không thể chia nhỏ thành các phần nhỏ hơn.
Khi có gì như vi phạm ràng buộc CHECK xảy ra ở giữa transaction, cách làm chuẩn là ROLLBACK; để gỡ bỏ mọi thứ an toàn.
-- Chuyển khoản: Alice -> Bob, 100 (chạy trên RDBMS thật)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE owner = 'Alice';
UPDATE account SET balance = balance + 100 WHERE owner = 'Bob';
COMMIT; -- cả hai UPDATE được xác nhận cùng một khoảnh khắc
-- Hoàn tác: đổi ý / có gì đó sai
BEGIN;
UPDATE account SET balance = balance - 300 WHERE owner = 'Alice';
UPDATE account SET balance = balance + 300 WHERE owner = 'Bob';
ROLLBACK; -- cả hai UPDATE bị bỏ đi cùng nhau
SAVEPOINT — rollback chỉ một phần của transaction
SAVEPOINT name; đặt một dấu mốc giữa chừng transaction, và ROLLBACK TO name; sau đó rollback chỉ đến dấu mốc đó.
Các thay đổi trước dấu mốc vẫn được giữ, nên bạn có thể tiếp tục các thao tác khác và COMMIT; ở cuối.
Một ứng dụng điển hình: quy trình nhiều bước như "xác nhận đơn hàng → cộng điểm → giảm tồn kho", trong đó chỉ bước giảm tồn kho thất bại và bạn muốn thử lại chỉ bước đó. Nó tinh tế hơn so với vứt bỏ cả transaction bằng ROLLBACK;.
-- Chỉ hoàn tác lần update thứ hai với SAVEPOINT (chạy trên RDBMS thật)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE owner = 'Carol';
SAVEPOINT sp1;
UPDATE account SET balance = balance - 100 WHERE owner = 'Dave';
ROLLBACK TO sp1; -- chỉ hoàn tác Dave (Carol -100 giữ lại)
COMMIT; -- chỉ xác nhận Carol -100
AUTOCOMMIT — mặc định, mỗi câu lệnh tự commit
Khi bạn chạy một UPDATE hay INSERT đứng riêng mà không viết BEGIN; trước, mỗi câu lệnh được commit tự động ngay.
Điều đó được gọi là AUTOCOMMIT — chế độ mặc định trong đó mỗi câu lệnh được commit ngay khi chạy.
Để commit hoặc hoàn tác nhiều câu lệnh như một đơn vị, bạn phải khởi động transaction tường minh với BEGIN;.
Khóa và deadlock — cơ bản về truy cập đồng thời
Cơ sở dữ liệu trong thế giới thực bị nhiều người dùng truy cập cùng lúc. Giả sử Alice có số dư 1000, và hai cửa hàng (X và Y) cố tính phí cô ấy cùng một khoảnh khắc — cả hai có thể đọc "1000" và trừ đi, và một trong các khoản trừ đó âm thầm biến mất.
Phòng vệ chống lại điều này là khóa: trong khi transaction của một người dùng đang chạm vào một dòng, bất kỳ ai khác cố chạm vào cùng dòng đó phải đợi. Người vào trước khóa dòng, và người thứ hai đợi.
Nhưng có mặt trái: nếu người dùng A khóa các tài khoản theo thứ tự "account 1 → account 2" và người dùng B khóa chúng theo thứ tự "account 2 → account 1", họ kết cục đợi khóa của nhau mãi mãi. Đó là deadlock. Cách khắc phục cổ điển là luôn lấy khóa theo cùng thứ tự (ví dụ, luôn theo account_id tăng dần). Nếu deadlock xảy ra, cơ sở dữ liệu sẽ buộc hủy một trong các transaction để phá vòng lặp.
Mức cô lập và ACID — tóm tắt thuật ngữ
Mức cô lập kiểm soát một transaction nhất định có thể thấy bao nhiêu công việc đang diễn ra của các transaction đồng thời khác.
Thiết lập yếu hơn (như READ COMMITTED) nhanh hơn nhưng cho phép bạn thấy nhiều thay đổi của người khác hơn, trong khi thiết lập mạnh hơn (như SERIALIZABLE) an toàn hơn nhưng buộc phải đợi nhiều hơn.
Cùng với tính nguyên tử (A), tính nhất quán (C), tính cô lập (I), và tính bền vững (D — một khi COMMIT xảy ra, dữ liệu nằm trên đĩa và sống sót qua sự cố hoặc mất điện), bốn cái này tạo nên các thuộc tính ACID.
Hành vi chính xác của khóa và mức cô lập khác nhau theo sản phẩm, nên khi bạn xây dựng thứ gì đó cho thực tế, hãy kiểm tra spec của cơ sở dữ liệu bạn đang dùng.
| ACID | Ý nghĩa |
|---|---|
| A — Atomicity (tính nguyên tử) | Các câu lệnh giữa BEGIN và COMMIT hoặc tất cả áp dụng hoặc không cái nào. |
| C — Consistency (tính nhất quán) | Ràng buộc CHECK, khóa ngoại, và các quy tắc toàn vẹn khác đúng trước và sau transaction. |
| I — Isolation (tính cô lập) | Công việc đang diễn ra của các transaction đồng thời khác được ẩn theo mức cô lập. |
| D — Durability (tính bền vững) | Một kết quả đã commit nằm trên đĩa và sống sót qua mất điện hoặc sự cố. |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Điều gì xảy ra với bảng khi bạn chạy BEGIN; UPDATE ...; UPDATE ...; ROLLBACK;?
Câu 3Điều gì xảy ra khi bạn chạy một UPDATE đứng riêng, không viết BEGIN; trước?