1. 程式人生 > >變與不變: Undo構造一致性讀的例外情況

變與不變: 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:

640?wx_fmt=png

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認為這種優化只是針對主鍵或唯一索引等行級訪問生效,造成資料一致性破壞的可能性很小,但是建議對於一致性要求較高的行業尤其是金融相關行業還是將該特性關閉,避免因此造成的一致性問題。

近期文章