Giao diện
6. Transaction, isolation và MVCC
Nhiều request cùng đọc và ghi một dòng thì mỗi bên thấy gì? InnoDB trả lời bằng MVCC cho đọc thường và lock cho ghi.
ACID
| Ý nghĩa | InnoDB đảm bảo bằng | |
|---|---|---|
| Atomicity | Tất cả hoặc không gì cả | Undo log lưu giá trị cũ để rollback |
| Consistency | Ràng buộc (PK, UNIQUE, FK, CHECK) luôn đúng trước và sau transaction | Phần lớn do schema và app |
| Isolation | Transaction chạy song song không thấy trạng thái dở của nhau | MVCC cho đọc thường, lock cho ghi và locking read |
| Durability | Commit xong thì sập máy vẫn còn | Redo log được fsync lúc commit (chương 8) |
Autocommit mặc định bật: mỗi câu lệnh lẻ là một transaction. Nhiều câu cần đi cùng nhau thì bọc trong BEGIN … COMMIT.
MVCC qua một ví dụ
Hai transaction cùng đọc và sửa số dư tài khoản id = 1 (ban đầu 100). Chọn isolation level, chạy từng bước và xem T1 nhìn thấy phiên bản nào trong chuỗi version.
| # | T1 (trx 201) | T2 (trx 202) | T1 thấy |
|---|---|---|---|
| 1 | BEGIN; SELECT balance FROM accounts WHERE id = 1; | ||
| 2 | BEGIN; UPDATE accounts SET balance = 50 WHERE id = 1; | ||
| 3 | SELECT balance FROM accounts WHERE id = 1; | ||
| 4 | COMMIT; | ||
| 5 | SELECT balance FROM accounts WHERE id = 1; | ||
| 6 | SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; | ||
| 7 | UPDATE accounts SET balance = balance - 10 WHERE id = 1; COMMIT; |
Chuỗi version của dòng id = 1 (mới nhất bên trái)
balance = 100trx_id 100đã commit từ trướctrên page
- Isolation
- REPEATABLE READ
- Read view của T1
- chưa có
Bấm “Bước tiếp”. Số dư ban đầu là 100.
Cơ chế: mỗi version của dòng ghi lại trx_id của transaction đã tạo ra nó và con trỏ roll_ptr tới version cũ hơn trong undo log. Một read view ghi lại các transaction đang chạy tại thời điểm nó được tạo. Khi đọc, InnoDB đi từ version mới nhất về sau, dừng ở version đầu tiên đã commit trước khi read view ra đời (hoặc do chính transaction đó tạo).
Bẫy lost update
Cảnh báo
Ở REPEATABLE READ, bước 5 T1 đọc được 100 nhưng bước 6 (FOR UPDATE) lại thấy 50. Locking read và UPDATE luôn làm việc trên bản mới nhất đã commit, không theo snapshot. Nếu app đọc 100 ở bước 5, tự tính 100 − 10 = 90 rồi SET balance = 90, khoản trừ 50 của T2 biến mất.
Ba cách làm đúng:
- Để database tính:
UPDATE accounts SET balance = balance - 10 WHERE id = 1 AND balance >= 10. - Khoá trước khi tính:
SELECT balance … FOR UPDATEtrong cùng transaction. - Optimistic lock bằng cột
version(chương 7).
Isolation level trong InnoDB
| Mức | Dirty read | Non-repeatable read | Phantom | Ghi chú |
|---|---|---|---|---|
| READ UNCOMMITTED | có | có | có | Đọc thẳng bản mới nhất, kể cả chưa commit |
| READ COMMITTED | không | có | có | Mỗi câu SELECT tạo read view mới; không dùng gap lock. Mặc định của PostgreSQL, Oracle |
| REPEATABLE READ | không | không | hầu như không | Mặc định InnoDB. Read view tạo ở lần đọc đầu; locking read dùng next-key lock chặn insert vào khoảng |
| SERIALIZABLE | không | không | không | SELECT thường (khi autocommit tắt) thành FOR SHARE, dễ chờ lock và deadlock |
Dirty read: đọc dữ liệu chưa commit. Non-repeatable read: đọc lại cùng dòng ra giá trị khác. Phantom: đọc lại cùng điều kiện thấy thêm dòng mới.
Giữ transaction ngắn
Transaction mở lâu giữ lock và giữ read view cũ. Purge thread không dọn được undo mà read view đó còn cần (history list length tăng), mọi SELECT phải đi qua chuỗi version dài hơn và hệ thống chậm dần. Không gọi API bên ngoài, không chờ người dùng thao tác khi đang mở transaction.
Ghi nhớ
- Mặc định InnoDB là REPEATABLE READ; SELECT thường đọc snapshot, không khoá.
- FOR UPDATE / UPDATE đọc bản mới nhất đã commit; đừng tính giá trị mới từ snapshot.
- RC tạo read view mỗi câu; RR tạo một lần.
- Transaction càng ngắn càng tốt.