ACID 的隔離性是可調的,預設擋不住 write skew

「資料庫自帶 ACID,所以我不用煩惱」是這句話最危險的讀法。交易的框架是自帶的,約束和隔離等級是你的責任。

先釐清:ACID 就是交易的定義

沒有交易就沒有 ACID 可言——它不是資料庫的背景屬性,是交易的屬性。而且你一直在用交易,只是沒寫出來:

UPDATE accounts SET balance = balance - 2000 WHERE id = 1;

autocommit 下這就是一筆完整的隱式交易,自己就有 A、C、I、D。所以要不要寫 BEGIN/COMMIT 的判準不是「要不要 ACID」,而是 這個業務動作需不需要跨多個 statement。

預設隔離等級擋不住的東西

典型教材例子:「訂房網站兩人同時訂同一間房,一定會有一人失敗」——在預設隔離等級下不成立。PostgreSQL/Oracle 預設 READ COMMITTED,MySQL 預設 REPEATABLE READ:

T1: SELECT ... WHERE room=5 AND date='10/1'  → 沒紀錄,可以訂
T2: SELECT ... WHERE room=5 AND date='10/1'  → 沒紀錄,可以訂
T1: INSERT ✅
T2: INSERT ✅   ← double booking

兩筆都是合法的 ACID 交易,但結果是錯的。 這叫 write skew(寫入偏斜):兩筆交易各自讀到的狀態都沒問題,衝突發生在它們之間。

三種修法,都要你主動做

ALTER TABLE bookings ADD CONSTRAINT uq UNIQUE (room_id, date);  -- 1. 唯一約束(最推薦)
SELECT ... WHERE room=5 FOR UPDATE;                             -- 2. 明確上鎖
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;                   -- 3. 提高隔離等級

要記的兩句

  • I 是可調的,而且預設比你以為的弱。
  • C 保護的只是你「宣告出來」的約束——沒建那條 UNIQUE,C 就完全幫不上忙。

第二句和 改漏一列的代價不是工多,是資料安靜地不一致 是同一個形狀:schema 設計得爛,ACID 的 C 一點忙都幫不上,因為它根本不知道你想保護什麼。