處理交易時的常見錯誤
這堂課我們會專注在處理交易時常見的錯誤,還有怎麼閃掉這些地雷。相信我,就算是超強的 SQL 大神,有時候也會忘記寫 COMMIT 啦!我們會給你一些 tips,讓交易出錯變成超罕見的事。
很遺憾(或許也算幸運?),資料庫不是什麼魔法城堡,不是每次都能完美運作。交易出錯這種事,對新手來說超常見。我們來細細拆解一下。
忘記下 COMMIT 或 ROLLBACK
忘記結束交易,這根本是「經典老梗」啦。想像你在餐廳點了餐,結果服務生忘記給你帳單。在 PostgreSQL 的世界裡,這就代表資料庫「卡」在交易狀態,佔著資源還擋住其他操作。
錯誤範例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 哎呀!我們忘記加上 COMMIT 或 ROLLBACK。
當交易「卡住」時,資源鎖可能會擴散到整張表。如果 DBA 發現情況不妙,他可能會用「硬」的方式結束它。但最好不要搞到這種地步啦。
怎麼避免?
- 每次都要明確結束交易:
COMMIT或ROLLBACK。 - 用一些 client 工具,讓它自動提醒你有卡住的交易。
- 如果交易沒結束你就重啟 app,資料庫會自動幫你
ROLLBACK,但這對系統狀態來說不一定方便。
用錯 isolation level
選 isolation level 可能看起來很無聊,但它其實超重要,可以防止各種怪異現象。比如你用 READ UNCOMMITTED 處理重要的金流,可能會讀到「髒」資料,結果後來又被取消。
範例:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 讀取被其他交易改動的資料
SELECT balance FROM accounts WHERE account_id = 1;
-- 另一個交易做了 ROLLBACK,你的資料就失效了。
怎麼避免?
- 先搞清楚你的 app 對資料的要求有多高。
- 大部分情境用
READ COMMITTED,這樣就不會讀到「髒」資料。 - 如果真的很重要,或怕出現 phantom data,就用更嚴格的
REPEATABLE READ、SERIALIZABLE。
交易衝突跟鎖死
有時候兩個以上的交易會想改同一筆資料。這時 PostgreSQL 會把其中一個鎖住,等另一個結束。這就有可能發生 deadlock(互鎖)的狀況。
錯誤範例:
-- 第一個交易
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 第二個交易
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- 等待第一個交易結束...
如果兩個交易都卡著對方需要的資源,就會 deadlock。PostgreSQL 會偵測到,然後強制結束其中一個,並丟出訊息:
ERROR: deadlock detected
怎麼避免?
- 每次交易都照固定順序操作資料。
- 讓交易執行時間越短越好,這樣比較不會卡住。
SERIALIZABLE這種 isolation level,真的必要時再用。
SAVEPOINT 用錯
SAVEPOINT 超好用,可以部分回滾,但用錯會讓你一頭霧水。比如你忘記釋放 savepoint(RELEASE SAVEPOINT),就可能多出一些鎖或錯誤。
錯誤範例:
BEGIN;
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT my_savepoint;
-- 忘記釋放 SAVEPOINT!
怎麼避免?
- 用完
SAVEPOINT記得刪掉,不然就會卡著。 - 不要亂設一堆 savepoint,不然查詢會變超複雜。
交易跟外部系統不合拍
想像一下,PostgreSQL 的交易要跟外部系統互動:發通知、更新 API 什麼的。如果外部系統出包,要回滾就很難了。
範例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 發通知失敗:email-server 沒回應。
COMMIT; -- 資料已經存進去了,但通知沒發出去。
怎麼避免?
- 能分開就分開,外部系統的操作盡量不要跟交易綁死。
- 用中介表或任務 queue 來協調跟外部系統的動作。
大交易帶來的錯誤
一次包太多操作的大交易,超容易出錯:鎖住、timeout、deadlock 都來。
範例:
BEGIN;
-- 幾千筆更新操作
UPDATE orders SET status = '已完成' WHERE delivery_date < CURRENT_DATE;
COMMIT; -- 可能會等超久。
怎麼避免?
- 把大交易拆成幾個小交易。
- 用 batch 處理資料更新。
- 一個交易裡改的資料越少越好。
忘記檢查錯誤
不是每個 SQL 查詢都會成功。如果其中一個操作出錯,整個交易就 fail 了。
範例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = -1; -- 錯誤:account_id 不存在。
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- 因為錯誤沒辦法執行。
怎麼避免?
- 每個操作都要檢查結果。
- 在查詢或 client code 裡加上錯誤處理。
搞錯 ROLLBACK 的行為
很多人以為 ROLLBACK 會把所有改動都還原。其實 ROLLBACK 只對當前交易有效。
誤解範例:
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK; -- 錯誤!這沒用,因為操作沒包在交易裡。
怎麼避免?
- 記住:
BEGIN是好朋友,沒它ROLLBACK沒轍。 - 重要操作都要包在交易裡。
GO TO FULL VERSION