Trong bài giảng này, tụi mình sẽ tập trung vào những lỗi điển hình hay gặp khi làm việc với transaction, và cách để né mấy lỗi đó. Tin mình đi, ngay cả SQL-master xịn sò đôi khi cũng quên COMMIT đó nha! Tụi mình sẽ chia sẻ vài tips để biến lỗi transaction thành chuyện hiếm hoi thôi.
Tiếc là (hoặc cũng có thể là may), database không phải là lâu đài phép thuật, nơi mọi thứ luôn chạy mượt mà. Lỗi khi làm việc với transaction là chuyện thường gặp, nhất là với mấy bạn mới học. Cùng phân tích kỹ hơn nhé.
Quên lệnh COMMIT hoặc ROLLBACK
Quên kết thúc transaction đúng là "kinh điển". Hãy tưởng tượng bạn đi ăn nhà hàng, gọi món xong mà phục vụ quên mang hóa đơn. Trong thế giới PostgreSQL, điều này có nghĩa là database "treo" ở trạng thái transaction, giữ tài nguyên và khóa các thao tác khác.
Ví dụ lỗi:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Ối dồi ôi! Quên mất COMMIT hoặc ROLLBACK rồi.
Khi transaction "treo", việc khóa tài nguyên có thể lan ra cả bảng luôn. Nếu admin database biết có chuyện không ổn, họ có thể kết thúc nó kiểu "cứng". Nhưng tốt nhất là đừng để tới mức đó.
Làm sao để tránh?
- Luôn luôn kết thúc transaction rõ ràng:
COMMIThoặcROLLBACK. - Dùng mấy tool client có nhắc nhở tự động về transaction đang treo.
- Nếu transaction chưa kết thúc mà bạn restart app, database sẽ tự động
ROLLBACK, nhưng đôi khi sẽ không tiện cho trạng thái hệ thống đâu.
Dùng sai isolation level
Chọn isolation level nghe có vẻ nhàm chán, nhưng nó cực kỳ quan trọng để tránh các anomaly. Ví dụ, nếu bạn dùng READ UNCOMMITTED cho giao dịch tài chính quan trọng, có thể bạn sẽ đọc phải dữ liệu "bẩn", sau này lại bị rollback.
Ví dụ:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- Đọc dữ liệu đang bị transaction khác thay đổi
SELECT balance FROM accounts WHERE account_id = 1;
-- Transaction khác sẽ ROLLBACK, và dữ liệu của bạn sẽ không còn hợp lệ.
Làm sao để tránh?
- Xác định xem dữ liệu quan trọng cỡ nào với app của bạn.
- Dùng
READ COMMITTEDcho đa số trường hợp để tránh đọc "bẩn". - Dùng isolation level nghiêm ngặt hơn như
REPEATABLE READ,SERIALIZABLEcho các thao tác cần tránh thay đổi hoặc phantom data.
Conflict transaction và lock
Đôi khi hai hoặc nhiều transaction cùng muốn sửa một dữ liệu. Lúc này PostgreSQL sẽ khóa một transaction lại, chờ transaction kia xong. Điều này có thể dẫn đến deadlock (khóa chéo lẫn nhau).
Ví dụ lỗi:
-- Transaction đầu tiên
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Transaction thứ hai
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- Đang chờ transaction đầu tiên...
Nếu cả hai transaction giữ tài nguyên mà bên kia cần, sẽ bị deadlock. PostgreSQL sẽ phát hiện deadlock, kết thúc một transaction với lỗi và báo:
ERROR: deadlock detected
Làm sao để tránh?
- Luôn tuân thủ thứ tự thao tác cố định trong các transaction.
- Giảm thời gian thực thi transaction để giảm khả năng bị lock.
- Chỉ dùng isolation level
SERIALIZABLEkhi thực sự cần thiết thôi nha.
Lỗi với SAVEPOINT
SAVEPOINT là công cụ xịn để rollback từng phần, nhưng dùng sai thì dễ rối lắm. Ví dụ, nếu bạn quên giải phóng savepoint (RELEASE SAVEPOINT), có thể gây thêm lock hoặc lỗi không đáng có.
Ví dụ lỗi:
BEGIN;
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT my_savepoint;
-- Quên giải phóng SAVEPOINT rồi!
Làm sao để tránh?
- Nhớ xóa
SAVEPOINTnếu không cần nữa. - Đừng tạo quá nhiều savepoint, kẻo query rối tung lên.
Transaction không tương thích với hệ thống ngoài
Hãy tưởng tượng transaction trong PostgreSQL đang cố gắng tương tác với hệ thống ngoài: gửi thông báo, update API các kiểu. Nếu hệ thống ngoài gặp sự cố, rollback thay đổi sẽ rất khó.
Ví dụ:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Không gửi được thông báo: email-server không phản hồi.
COMMIT; -- Dữ liệu đã lưu, nhưng thông báo chưa gửi được.
Làm sao để tránh?
- Nếu có thể, tách biệt thao tác với hệ thống ngoài.
- Dùng bảng trung gian hoặc queue để phối hợp với hệ thống ngoài.
Lỗi do transaction quá lớn
Transaction lớn, gồm nhiều thao tác, dễ bị lỗi hơn: lock, timeout, deadlock các kiểu.
Ví dụ:
BEGIN;
-- Hàng ngàn thao tác update
UPDATE orders SET status = 'completed' WHERE delivery_date < CURRENT_DATE;
COMMIT; -- Có thể mất kha khá thời gian đó.
Làm sao để tránh?
- Chia transaction lớn thành nhiều transaction nhỏ.
- Dùng batch để update dữ liệu.
- Giảm tối đa lượng dữ liệu thay đổi trong một transaction.
Quên kiểm tra lỗi
Không phải query SQL nào trong transaction cũng thành công đâu nha. Ví dụ, nếu một thao tác bị lỗi, cả transaction sẽ fail luôn.
Ví dụ:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = -1; -- Lỗi: account_id không tồn tại.
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- Không thực hiện được vì lỗi.
Làm sao để tránh?
- Luôn kiểm tra kết quả từng thao tác.
- Dùng xử lý lỗi trong query hoặc code client của bạn.
Hiểu nhầm về ROLLBACK
Nhiều dev nghĩ ROLLBACK sẽ hoàn tác mọi thay đổi và trả về trạng thái ban đầu. Nhưng ROLLBACK chỉ hoạt động trong phạm vi transaction hiện tại thôi nha.
Ví dụ hiểu nhầm:
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK; -- Lỗi! Không hiệu quả vì thao tác không nằm trong transaction.
Làm sao để tránh?
- Nhớ nhé:
BEGINlà bạn thân, không có nó thìROLLBACKvô dụng. - Luôn bọc các thao tác quan trọng trong transaction.
GO TO FULL VERSION