變與不變: Undo構造一致性讀的例外情況

嘉年華聽了恩墨學院的一個主題:《重現ORA-01555 細說Oracle 12c Undo數據管理》,呂星昊老師介紹了UNDO的概念以及ORA-1555的產生,並介紹了12c以來Oracle的UNDO相關的新特性。

其中介紹了Oracle如何使用UNDO來實現多版本一致性讀,使用了OPEN CURSOR的方式非常巧妙地在很少量數據的情況下構造出可重現的案例。不過這個案例存在一點小的瑕疵,因爲如果一不小心,很可能會導致結果與預期不符,這是因爲這裏有一個例外存在。

我們先來模擬一下UNDO構造一致性讀的情況,對於Oracle而言,默認的隔離級別是READ COMMIT,也就是說一個會話只能看到其他會話已經提交的修改,未提交的修改或者在當前會話查詢發起之後提交的修改都是不可見的。

再介紹一下OPEN CURSOR,Oracle中當一個遊標被打開,其結果集就已經確定了,也就是說這個遊標會根據OPEN CURSOR這個時間點對應的SCN來構造一致性查詢。但是OPEN CURSOR時,對應的SQL並不會被執行,在後續FETCH的時候(對於SQLPLUS而言PRINT命令會觸發FETCH),SQL才真正被執行。使用這種辦法可以模擬一個大的查詢,OPEN CURSOR相當於大的查詢的開始時間,其早於其他會話的修改提交時間,而FETCH的時間相當於大查詢讀取到這條記錄的時間,而該時間晚於其他會話提交的時間:

SQL> SET SQLP 'SQL1> ' SQL1> CREATE TABLE T_UNDO (ID NUMBER, NAME VARCHAR2(30)); Table created. SQL1> INSERT INTO T_UNDO SELECT ROWNUM, OBJECT_NAME FROM DBA_OBJECTS; 96920 rows created. SQL1> COMMIT; Commit complete. SQL1> CREATE INDEX IND_UNDO_ID ON T_UNDO(ID); Index created. SQL1> SELECT NAME FROM T_UNDO WHERE ID = 1119; NAME ------------------------------------------------------------ I_EXTERNAL_LOCATION1$ SQL1> VAR C REFCURSOR SQL1> EXEC OPEN :C FOR SELECT NAME FROM T_UNDO WHERE ID = 1119; PL/SQL procedure successfully completed.

在第一個會話已經構造了一個查詢,下面在會話2對這條ID爲1119的記錄進行修改並提交:

SQL> SET SQLP 'SQL2> ' SQL2> UPDATE T_UNDO SET NAME = 'UPDATED' WHERE ID = 1119; 1 row updated. SQL2> COMMIT; Commit complete.

在會話3上執行查詢,這時會看到會話2修改提交後的結果:

SQL> SET SQLP 'SQL3> ' SQL3> SELECT NAME FROM T_UNDO WHERE ID = 1119; NAME ------------------------------------------------------------ UPDATED

回到會話1,對CURSOR變量執行PRINT,檢查得到的結果:

SQL1> PRINT :C NAME ------------------------------------------------------------ I_EXTERNAL_LOCATION1$

到目前爲止,所有都是預期之內的結果,Oracle會利用UNDO來存儲UPDATE的前鏡像,當查詢發現需要訪問的數據塊SCN大於會話發起的SCN,而需要通過UNDO中存儲的前鏡像來構造一致性讀,找到會話需要讀取的修改前的數據。

那麼例外來自哪裏呢,在這個例子中,我們給ID列上創建了一個索引,如果這不是一個普通的索引,而是一個主鍵,那麼效果如何呢:

SQL1> DROP INDEX IND_UNDO_ID; Index dropped. SQL1> ALTER TABLE T_UNDO ADD PRIMARY KEY (ID); Table altered. SQL1> SELECT NAME FROM T_UNDO WHERE ID = 1118; NAME ------------------------------------------------------------ EXTERNAL_LOCATION$ SQL1> EXEC OPEN :C FOR SELECT NAME FROM T_UNDO WHERE ID = 1118; PL/SQL procedure successfully completed.

會話2修改ID爲1118的記錄:

SQL2> UPDATE T_UNDO SET NAME = 'UPDATED WITH PK' WHERE ID = 1118; 1 row updated. SQL2> COMMIT; Commit complete.

會話3檢查確認修改結果:

SQL3> SELECT NAME FROM T_UNDO WHERE ID = 1118; NAME --------------- UPDATED WITH PK

再次回到會話1,PRINT遊標變量:

SQL1> PRINT :C NAME ------------------------------------------------------------ UPDATED WITH PK

可以看到例外產生了,一致性讀的結果被破壞了,居然可以查詢到發生在遊標打開之後提交的修改。

導致這個例外的原因來自於一個隱含函數_row_cr:

Oracle11g以後,這個隱含參數默認值修改爲TRUE,這使得Oracle對於基於主鍵的訪問不再採用默認的一致性讀方案。當然Oracle做出這種修改的目的是爲了提高性能,而且僅對於單行訪問生效,而大部分情況下單行訪問的效率非常高,因此對於一致性破壞的影響並不明顯。到18C爲止,該參數仍然爲TRUE。

如果關閉該參數:

SQL1> ALTER SYSTEM SET "_row_cr" = FALSE; System altered. SQL1> SELECT NAME FROM T_UNDO WHERE ID = 1117; NAME ------------------------------------------------------------ I_EXTERNAL_TAB1$ SQL1> EXEC OPEN :C FOR SELECT NAME FROM T_UNDO WHERE ID = 1117; PL/SQL procedure successfully completed.

會話2進行修改:

SQL2> UPDATE T_UNDO SET NAME = 'UPDATED NO ROW CR' WHERE ID = 1117; 1 row updated. SQL2> COMMIT; Commit complete.

檢查結果:

SQL3> SELECT NAME FROM T_UNDO WHERE ID = 1117; NAME ------------------ UPDATED NO ROW CR

回到會話1檢查結果:

SQL1> PRINT :C NAME ------------------------------------------------------------ I_EXTERNAL_TAB1$

Oracle恢復默認的讀一致性隔離級別。

雖然Oracle認爲這種優化只是針對主鍵或唯一索引等行級訪問生效,造成數據一致性破壞的可能性很小,但是建議對於一致性要求較高的行業尤其是金融相關行業還是將該特性關閉,避免因此造成的一致性問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章