Trong bài này
Cô Mai chuyển một khoản xu từ học viên này sang học viên khác trong dữ liệu thử. Trừ người gửi xong mà chưa cộng người nhận sẽ khiến công việc bị dang dở. Cô cần xác nhận cả hai bước cùng nhau, hoặc hủy chúng nếu không thể hoàn tất.
Xác nhận cả nhóm thay đổi ở một ranh giới
Giao dịch (transaction) là một nhóm thao tác được xác nhận hoặc hủy trong cùng một phạm vi. Ta bắt đầu bằng BEGIN, thực hiện các bước ghi, rồi xác nhận bằng COMMIT. Sau khi xác nhận thành công, nhóm thao tác kết thúc.
Chuyển xu cần ít nhất hai phép sửa. Bạn giảm số dư người gửi rồi tăng số dư người nhận bằng cùng một lượng. Hai câu có thể đúng riêng lẻ, nhưng chỉ hoàn thành công việc khi đi cùng nhau.
Trong ví dụ này, chuyển 10 xu từ học viên mã 1 sang mã 2:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
BEGIN;
UPDATE students
SET coins = coins - 10
WHERE id = 1;
UPDATE students
SET coins = coins + 10
WHERE id = 2;
COMMIT;
SELECT id, name, coins
FROM students
WHERE id IN (1, 2)
ORDER BY id;Bảng students sau khi chạy
students2 dòng thay đổi| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 110 | Cô Mai |
| 2 | Bình | 7 | Đà Nẵng | 100 | NULL |
| 3 | Chi | 8 | Hà Nội | 180 | Thầy Nam |
| 4 | Dung | 6 | Huế | 60 | NULL |
| 5 | Huy | 9 | Cần Thơ | 180 | Cô Mai |
| 6 | Lan | 7 | Hà Nội | 150 | Thầy Nam |
| id | name | coins |
|---|---|---|
| 1 | An | 110 |
| 2 | Bình | 100 |
An còn 110 xu và Bình có 100 xu. Tổng của hai người vẫn là 210, vì cùng một lượng được chuyển giữa hai phía. Những người khác không đổi, do hai điều kiện chỉ chọn đúng mã gửi và nhận.
COMMIT phải đứng sau cả hai thay đổi nếu chúng thuộc cùng một công việc. Đặt nó giữa hai câu sẽ kết thúc nhóm quá sớm. Câu cộng sau đó không còn được bảo vệ cùng câu trừ trong transaction vừa xác nhận.
BEGINMở nhóm thao tácTrừ xuSửa số dư người gửiCộng xuSửa số dư người nhậnKiểm kết quảĐối chiếu các điều kiệnCOMMITXác nhận cả nhóm
Ranh giới nên bao trọn phần dữ liệu phải cùng thay đổi. Nếu công việc còn yêu cầu ghi lịch sử, phần ghi ấy cũng cần nằm trong phạm vi phù hợp. Bài tổng hợp sau sẽ kết hợp số dư với lịch sử thay đổi.
Hủy toàn bộ phần việc chưa xác nhận
Hoàn tác (rollback) là đưa dữ liệu trở về trạng thái trước nhóm thay đổi chưa xác nhận. Lệnh ROLLBACK không có TO hủy toàn bộ transaction đang mở. Nó khác việc viết thêm các phép cộng trừ ngược lại để đoán trạng thái ban đầu.
Giả sử cô Mai chỉ đang thử một lần chuyển 15 xu. Kết thúc bằng hủy thay vì xác nhận sẽ giữ dữ liệu ban đầu:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
BEGIN;
UPDATE students
SET coins = coins - 15
WHERE id = 1;
UPDATE students
SET coins = coins + 15
WHERE id = 2;
ROLLBACK;
SELECT id, name, coins
FROM students
WHERE id IN (1, 2)
ORDER BY id;Bảng students sau khi chạy
students6 dòng| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 120 | Cô Mai |
| 2 | Bình | 7 | Đà Nẵng | 90 | NULL |
| 3 | Chi | 8 | Hà Nội | 180 | Thầy Nam |
| 4 | Dung | 6 | Huế | 60 | NULL |
| 5 | Huy | 9 | Cần Thơ | 180 | Cô Mai |
| 6 | Lan | 7 | Hà Nội | 150 | Thầy Nam |
| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
Kết quả lại là 120 xu của An và 90 xu của Bình. Hai câu sửa đã chạy trong transaction, nhưng chưa được xác nhận. ROLLBACK hủy chúng và đóng nhóm thao tác, nên không cần thêm COMMIT cho nhóm vừa hủy.
Nếu bạn đọc dữ liệu bên trong transaction trước khi hủy, kết nối ấy có thể thấy số dư tạm thời. Thấy kết quả đúng trong lúc đang mở không có nghĩa công việc đã được xác nhận. Bài nộp phải kết thúc transaction rõ ràng.
| Lệnh kết thúc | Thay đổi trong transaction | Transaction sau lệnh |
|---|---|---|
COMMIT thành công | Được xác nhận | Đã đóng |
ROLLBACK toàn bộ | Bị hủy | Đã đóng |
Chỉ chạy SELECT | Chưa có quyết định kết thúc | Vẫn có thể đang mở |
Savepoint cho phép quay lại một mốc trong nhóm
Mốc lưu (savepoint) là một vị trí có tên trong transaction để có thể quay lại sau đó. SAVEPOINT tạo mốc, ROLLBACK TO hủy thay đổi sau mốc, còn RELEASE bỏ mốc. Các bước đã thực hiện trước mốc vẫn được giữ trong transaction ngoài.
Ví dụ, cô Mai quyết định cộng 5 xu cho học viên mã 1. Sau đó cô thử cộng thêm 10 xu cho cùng người, nhưng muốn bỏ riêng phần thử. Đặt mốc sau khoản chắc chắn cần giữ:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
BEGIN;
UPDATE students
SET coins = coins + 5
WHERE id = 1;
SAVEPOINT thu_them;
UPDATE students
SET coins = coins + 10
WHERE id = 1;
ROLLBACK TO thu_them;
RELEASE thu_them;
COMMIT;
SELECT id, name, coins
FROM students
WHERE id = 1;Bảng students sau khi chạy
students1 dòng thay đổi| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 125 | Cô Mai |
| 2 | Bình | 7 | Đà Nẵng | 90 | NULL |
| 3 | Chi | 8 | Hà Nội | 180 | Thầy Nam |
| 4 | Dung | 6 | Huế | 60 | NULL |
| 5 | Huy | 9 | Cần Thơ | 180 | Cô Mai |
| 6 | Lan | 7 | Hà Nội | 150 | Thầy Nam |
| id | name | coins |
|---|---|---|
| 1 | An | 125 |
Kết quả cuối là 125 xu. Lệnh quay lại chỉ hủy khoản tăng sau SAVEPOINT, nên khoản tăng đầu vẫn còn. Sau khi bỏ mốc, COMMIT xác nhận phần còn lại của transaction ngoài.
ROLLBACK TO không tự bỏ chính mốc vừa quay về. Bạn có thể tiếp tục sửa hoặc giải phóng mốc bằng RELEASE. Trong ví dụ, giải phóng mốc giúp thể hiện rõ rằng phần thử đã kết thúc, nhưng transaction ngoài vẫn cần quyết định xác nhận.
Mốc phải được đặt trước phần có thể cần hủy. Nếu đặt sau cả hai khoản tăng, quay về mốc sẽ không hủy khoản nào trong hai khoản ấy. Vị trí mốc quan trọng hơn tên mốc mà bạn chọn.
- Đặt mốc sau phần chắc chắn muốn giữ.
- Thực hiện phần có thể cần thử lại hoặc bỏ đi.
- Quay về mốc nếu muốn hủy phần sau đó.
- Đóng transaction ngoài khi toàn bộ công việc đã sẵn sàng.
Trước khi chạy, bạn có thể vạch từng ranh giới trên giấy. Đánh dấu phần cần giữ, phần có thể bỏ và thời điểm được phép xác nhận. Sau đó đọc lần lượt các lệnh từ trên xuống để dự đoán trạng thái cuối. Cách này giúp phát hiện một mốc đặt sai chỗ trước khi nhìn vào con số kết quả.
Bỏ mốc bên trong chưa phải xác nhận toàn bộ
Trong một transaction mở bằng BEGIN, RELEASE mốc bên trong không thay thế COMMIT. Nó chỉ bỏ khả năng quay lại mốc ấy. Transaction ngoài vẫn có thể bị hủy, kể cả phần đã đi qua mốc được giải phóng.
Hãy quan sát việc cộng xu rồi giải phóng mốc, nhưng cuối cùng hủy transaction ngoài:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
BEGIN;
SAVEPOINT thu_thuong;
UPDATE students
SET coins = coins + 5
WHERE id = 2;
RELEASE thu_thuong;
ROLLBACK;
SELECT id, name, coins
FROM students
WHERE id = 2;Bảng students sau khi chạy
students6 dòng| id | name | grade | city | coins | mentor |
|---|---|---|---|---|---|
| 1 | An | 6 | Hà Nội | 120 | Cô Mai |
| 2 | Bình | 7 | Đà Nẵng | 90 | NULL |
| 3 | Chi | 8 | Hà Nội | 180 | Thầy Nam |
| 4 | Dung | 6 | Huế | 60 | NULL |
| 5 | Huy | 9 | Cần Thơ | 180 | Cô Mai |
| 6 | Lan | 7 | Hà Nội | 150 | Thầy Nam |
| id | name | coins |
|---|---|---|
| 2 | Bình | 90 |
Bình vẫn có 90 xu như trước. Khoản tăng nằm trong transaction ngoài chưa xác nhận nên ROLLBACK vẫn hủy được nó. Nếu coi RELEASE là xác nhận toàn bộ, bạn sẽ dự đoán sai rằng khoản tăng còn được giữ.
Một lỗi câu lệnh không luôn hủy cả transaction
Nhiều lỗi ràng buộc mặc định của SQLite chỉ hủy câu đang lỗi. Những câu thành công trước đó trong transaction có thể vẫn chờ xác nhận. Ứng dụng phải bắt lỗi và chọn hủy toàn bộ nếu yêu cầu các bước cùng thành công.
Lỗi còn có thể đến từ cách nhóm lệnh. SQLite không cho lồng một BEGIN mới trong transaction đang mở. Nếu cần một mốc bên trong, hãy dùng SAVEPOINT thay vì mở transaction thứ hai:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
BEGIN;
BEGIN;Thông báo lỗi
cannot start a transaction within a transaction
Thông báo lỗi cho thấy cách mở nhóm lồng nhau này không hợp lệ. Nó không chứng minh mọi lỗi đều tự đóng transaction ngoài. Trong ứng dụng, đường xử lý lỗi cần kiểm trạng thái và hủy phần việc chưa hoàn tất.
Giữ transaction ngắn giúp giảm thời gian giữ tài nguyên. Bạn nên chuẩn bị dữ liệu nhập trước, rồi chỉ nhóm những thao tác đọc và ghi cần thiết. Chờ người dùng trả lời hoặc gọi một dịch vụ chậm trong lúc đang mở có thể kéo dài phạm vi chưa kết thúc.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Xác nhận giữa hai bước chuyển xu | Hai bước không còn chung phạm vi | Đặt COMMIT sau cả hai |
Dùng ROLLBACK khi chỉ muốn bỏ phần thử | Hủy cả việc trước mốc | Dùng ROLLBACK TO đúng mốc |
Tưởng RELEASE bên trong đã xác nhận | Transaction ngoài vẫn mở | Kết thúc phạm vi ngoài rõ ràng |
Dùng BEGIN để tạo nhóm lồng | SQLite không cho cách lồng này | Tạo mốc bằng SAVEPOINT |
Tóm tắt
- Transaction nhóm phần dữ liệu phải được xác nhận hoặc hủy cùng nhau.
ROLLBACKtoàn bộ đóng nhóm và hủy các thay đổi chưa xác nhận.ROLLBACK TOgiữ phần trước mốc và để transaction ngoài tiếp tục.- Giải phóng mốc bên trong không bảo vệ thay đổi khỏi một lần hủy toàn bộ sau đó.
- Đường xử lý lỗi của ứng dụng phải quyết định rõ có hủy cả công việc hay không.