承接前文,本文展示更隱蔽的邏輯衝突:兩條語義等價的 UPDATE,因一致性讀快照與當前模式讀的時間差,在並發下產生「邏輯上應相同的列卻寫入不同值」的資料不一致現象。
-- 目標:讓 object_id=101 和 102 的 lio 值永遠相同
-- 方法 1:單一 UPDATE 設定常數
UPDATE t_test1 SET lio=0 WHERE object_id IN (101,102);
-- 方法 2:子查詢複製 101 的值給 102(含延遲子查詢)
UPDATE t_test1
SET lio = (SELECT lio FROM t_test1 WHERE object_id=101)
WHERE object_id = 102
AND (SELECT COUNT(*) FROM t_test2 t1, t_test2 t2) > 0;
UPDATE t_test1 SET lio=1 WHERE object_id IN (101,102); COMMIT; → 兩列 lio=1。SET lio=0)並提交。SELECT object_id, lio FROM t_test1 WHERE object_id IN (101,102);
-- 結果:
OBJECT_ID LIO
--------- ---
101 0 -- 被 Session 2 更新為 0
102 1 -- 被 Session 1 更新為 1(來自一致性讀快照中的 101 值)
WHERE object_id=102,同時執行子查詢 SELECT lio FROM t_test1 WHERE object_id=101 → 讀到 lio=1(SCN=100 快照)。UPDATE ... SET lio=0 WHERE object_id IN (101,102),SCN=105 提交。兩列在當前模式下均變為 0。WHERE 條件(仍成立),執行 SET lio = 1(使用階段一讀到的快照值 1)。非缺陷,而是設計權衡:MVCC 犧牲「讀寫一致性」換取「讀不阻塞寫、寫不阻塞讀」的高併發吞吐。Oracle 文件明確說明:UPDATE 的 SET 子句使用查詢啟動時的一致性快照值,而非當前值。
| 方案 | 實作 | 優點 | 缺點 |
|---|---|---|---|
| 樂觀鎖版本欄 | ADD version NUMBER; UPDATE ... SET lio=..., version=version+1 WHERE id=? AND version=? | 應用層可感知衝突、重試 | 需修改表結構、應用碼 |
| SELECT FOR UPDATE 先鎖 | SELECT lio FROM t_test1 WHERE object_id=101 FOR UPDATE; 再 UPDATE | 序列化存取、強一致性 | 鎖競爭、降低併發 |
| SERIALIZABLE 隔離級 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; | 語句級可序列化 | ORA-08177 錯誤頻發、需重試邏輯 |
| 單一 UPDATE 避免子查詢 | UPDATE t_test1 SET lio=0 WHERE object_id IN (101,102); | 原子性、無快照問題 | 不適用「複製其他列值」場景 |
| 物化子查詢結果 | WITH src AS (SELECT lio FROM t_test1 WHERE object_id=101) UPDATE ... SET lio=(SELECT lio FROM src) ... | 縮短一致性讀窗口 | 仍有微小時間差 |
MERGE 或 SELECT ... FOR UPDATE 明確序列化。CHECK 約束或觸發器驗證,或應用層雙寫驗證。V$SEGMENT_STATISTICS 中 logical reads 暴增的物件,往往暗示長查詢導致一致性讀成本高。深入理解一致性讀快照「凍結」SET 子句值的機制,能從架構層面預防並發更新邏輯衝突,打造在高併發下依然邏輯自洽的核心交易系統。