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_idownerbalance
1Alice1000
2Bob500
3Carol800
4Dave1200

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.

Ngã rẽ COMMIT / ROLLBACK
BEGINthử các thay đổikiểm tra kết quảCOMMITxác nhận thay đổiROLLBACKquay về ban đầuổn cảhủy / lỗi
Sau BEGIN, COMMIT xác nhận các thay đổi; ROLLBACK đưa mọi thứ về trạng thái trước BEGIN. Ngay cả sau vi phạm CHECK, ROLLBACK hoàn tác 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;.

SAVEPOINT và ROLLBACK TO — rollback một phần
BEGINUPDATEAlice -100SAVEPOINT sp1dấu mốc ở đâyUPDATEBob -100ROLLBACK TO sp1chỉ hoàn tác phần của BobCOMMITchỉ xác nhận Alice -100hoàn táctiếp tục
Đặt một dấu mốc với SAVEPOINT và hoàn tác chỉ các thay đổi sau nó với ROLLBACK TO. Các thay đổi trước dấu mốc được giữ và có thể xác nhận với COMMIT ở cuối.
-- 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;.

AUTOCOMMIT vs. transaction tường minh
Không BEGIN(AUTOCOMMIT)Có BEGINtransaction tường minhUPDATEcommit ngay tại chỗUPDATEchưa commitkhông thể hoàn tácCOMMIT để xác nhậnROLLBACK để hoàn tác
Không có BEGIN, mỗi câu lệnh commit ngay lúc nó chạy. Với BEGIN, mọi thứ đến COMMIT trở thành một đơn vị bạn có thể rollback cùng nhau.

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.

Khóa và ngõ cụt của chúng (deadlock)
Người dùng Akhóa account 1Người dùng Bkhóa account 2A xin account 2và đợiB xin account 1và đợiDeadlockcả hai đợi mãi mãiDB buộc hủymột trong số đó
Khi một người dùng đang cập nhật một dòng, dòng đó bị khóa và người dùng khác phải đợi. Nếu hai transaction mỗi cái giữ thứ cái kia cần, cả hai đợi mãi mãi — một deadlock.

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 BEGINCOMMIT 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ố.
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

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?

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?