Transaction 交易控制
什麼是 Transaction?
比喻:Transaction 就像 ATM 轉帳 🏧
你從 A 帳戶轉 1000 元到 B 帳戶:
- A 帳戶 -1000
- B 帳戶 +1000
如果步驟 1 成功但步驟 2 失敗,錢就消失了! Transaction 確保「全部成功」或「全部失敗」。
ACID 四大特性
| 特性 | 英文 | 說明 |
|---|---|---|
| 原子性 | Atomicity | 全部成功或全部失敗,不會只做一半 |
| 一致性 | Consistency | 交易前後,資料保持一致(餘額不會變負) |
| 隔離性 | Isolation | 多個交易同時進行不會互相干擾 |
| 持久性 | Durability | 一旦 COMMIT,資料永久保存 |
基本語法
BEGIN; -- ← 開始交易
-- 步驟 1:A 扣款
UPDATE Accounts
SET Balance = Balance - 1000
WHERE Id = 1;
-- 步驟 2:B 入款
UPDATE Accounts
SET Balance = Balance + 1000
WHERE Id = 2;
COMMIT; -- ← 確認,全部生效
逐行解析:
BEGIN; -- 開始一個交易
-- 從此開始的所有操作都是「暫定的」
UPDATE ... (A 扣款) -- 暫時從 A 扣 1000
UPDATE ... (B 入款) -- 暫時給 B 加 1000
COMMIT; -- 全部沒問題 → 正式寫入資料庫
-- 如果中間任何一步出錯 → 用 ROLLBACK
ROLLBACK — 復原
BEGIN;
UPDATE Accounts SET Balance = Balance - 1000 WHERE Id = 1;
UPDATE Accounts SET Balance = Balance + 1000 WHERE Id = 2;
-- 發現轉錯人了!
ROLLBACK; -- ← 取消全部,回到 BEGIN 之前的狀態
SAVEPOINT — 存檔點
BEGIN;
INSERT INTO Orders (CustomerId, Amount) VALUES (1, 500);
SAVEPOINT sp1; -- ← 設一個存檔點
INSERT INTO OrderItems (OrderId, Product) VALUES (1, 'A');
INSERT INTO OrderItems (OrderId, Product) VALUES (1, 'B');
-- 發現商品 B 有問題
ROLLBACK TO sp1; -- ← 回到 sp1,只取消 OrderItems
-- Orders 的 INSERT 還在!
-- 重新插入正確的資料
INSERT INTO OrderItems (OrderId, Product) VALUES (1, 'C');
COMMIT; -- ← 最終:Orders + 商品 A 和 C
隔離等級(Isolation Level)
| 等級 | 說明 | 問題 |
|---|---|---|
| READ UNCOMMITTED | 可讀其他交易未提交的資料 | 髒讀 |
| READ COMMITTED | 只讀已提交的資料(PostgreSQL 預設) | 不可重複讀 |
| REPEATABLE READ | 同一交易中多次讀取結果相同 | 幻讀 |
| SERIALIZABLE | 完全隔離,像排隊一個一個來 | 效能最差 |
-- 設定隔離等級
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT Balance FROM Accounts WHERE Id = 1;
-- 在 SERIALIZABLE 下,其他交易無法修改這筆資料
UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1;
COMMIT;
實用範例:安全轉帳
CREATE OR REPLACE FUNCTION transfer_money(
from_id INT,
to_id INT,
amount DECIMAL
)
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
from_balance DECIMAL;
BEGIN
-- 檢查餘額
SELECT Balance INTO from_balance
FROM Accounts WHERE Id = from_id
FOR UPDATE; -- ← 鎖定這列,防止同時被改
IF from_balance < amount THEN
RAISE EXCEPTION '餘額不足!目前餘額:%', from_balance;
END IF;
-- 扣款
UPDATE Accounts SET Balance = Balance - amount WHERE Id = from_id;
-- 入款
UPDATE Accounts SET Balance = Balance + amount WHERE Id = to_id;
-- 記錄
INSERT INTO TransferLog (FromId, ToId, Amount, TransferAt)
VALUES (from_id, to_id, amount, CURRENT_TIMESTAMP);
END;
$$;
死鎖(Deadlock)
交易 A:鎖住 Row 1,等待 Row 2
交易 B:鎖住 Row 2,等待 Row 1
→ 互相等待,永遠不會結束 = 死鎖!
-- 預防死鎖:統一鎖定順序
-- ✅ 好:永遠先鎖 Id 小的
BEGIN;
SELECT * FROM Accounts WHERE Id = 1 FOR UPDATE;
SELECT * FROM Accounts WHERE Id = 2 FOR UPDATE;
COMMIT;
-- ❌ 壞:A 先鎖 1 再鎖 2,B 先鎖 2 再鎖 1 → 死鎖