1. 程式人生 > >oracle 並發與多版本

oracle 並發與多版本

having all autotrace bsp sleep 也看 sum 另一個 需求

並發控制 concurrency control
  數據庫提供的函數集合,允許多個人同時訪問和修改數據。

  鎖(lock)是Oracle管理共享數據庫資源並發訪問並防止並發數據庫事務之間“相互幹涉”的核心機制之一。

Oracle使用了多種鎖,包括:

1. TX鎖:修改數據的事務在執行期間會獲得這種鎖。
2. TM鎖和DDL鎖:在你修改一個對象的內容(對於TM鎖)或對象本身(對應DDL鎖)時,這些鎖可以確保對象的結構不被修改。
3. 閂(latch):這是Oracle的內部鎖,用來協調對其共享數據結構的訪問。

不論是哪一種鎖,請求鎖時都存在相關的最小開銷。

TX鎖在性能和基數方面可擴縮性極好。

TM鎖和DDL鎖要盡可能地采用限制最小的模式。

閂和隊列鎖(enqueue)都是輕量級的,而且都很快。


  但是Oracle對並發的支持不只是高效的鎖定。它還實現了一種多版本(multi-versioning)體系結構,這種體系結構提供了一種受控但高度並發的數據訪問。多版本是指,Oracle能同時物化多個版本的數據,這也是Oracle提供數據讀一致視圖的機制(讀一致視圖即read-consistent view,是指相對於某個時間點有一致的結果)。多版本有一個很好的副作用,即數據的讀取器(reader)絕對不會被數據的寫入器(writer)所阻塞。換句話說,寫不會阻塞讀。在Oracle中,如果一個查詢只是讀取信息,那麽永遠也不會被阻塞。它不會與其他會話發生死鎖,而且不可能得到數據庫中根本不存在的答案。
默認情況下,Oracle的讀一致性多版本模型應用於語句級(statement level),也就是說,應用於每一個查詢;另外還可以應用於事務級(transaction level)。這說明,至少提交到數據庫的每一條SQL語句都會看到數據庫的一個讀一致視圖,如果你希望數據庫的這種讀一致視圖是事務級的(一組SQL語句),這也是可以的。
數據庫中事務的基本作用是將數據庫從一種一致狀態轉變為另一種一種狀態。ISO SQL標準指定了多種事務隔離級別(transaction isolation level),這些隔離級別定義了一個事務對其他事務做出的修改有多“敏感”。越是敏感,數據庫在應用執行的各個事務之間必須提供的隔離程度就越高。
事務隔離級別
  ANSI/ISO SQL標準定義了4種事務隔離級別,對於相同的事務,采用不同的隔離級別分別有不同的結果。即使輸入相同,而且采用同樣的方式來完成同樣的工作,也可能得到完全不同的答案,這取決於事務的隔離級別。這些隔離級別是根據3個“現象”定義的,以下就是給定隔離級別可能允許或不允許的3種現象:

1. 臟讀(dirty read):能讀取未提交的數據。只要打開別人正在讀寫的一個OS文件,就會臟讀。臟讀,將影響數據完整性,外鍵約束會遭到破壞,會忽略惟一性約束。
2. 不可重復讀(nonrepeatable read):如果你在T1時間讀取某一行,在T2時間重新讀取這一行時,這一行可能已經有所修改。也許它已經消失,有可能被更新了,等等。
3. 幻像讀(phantom read):如果你在T1時間執行一個查詢,而在T2時間再執行這個查詢,此時可能已經向數據庫中增加了另外的行,這會影響你的結果。
  與不可重復讀的區別在於:在幻像讀中,已經讀取的數據不會改變,只是與以前相比,會有更多的數據滿足你的查詢條件。

隔離級別      臟讀 不可重復 幻像讀
READ UNCOMMITTED 允許 允許   允許 (讀未提交)級別用來得到非阻塞讀(non-blocking read)
READ COMMITTED   允許 允許   不能提供一致的結果
REPEATABLE READ  允許 可以保證由查詢得到讀一致的(read-consistent)結果
SERIALIZABLE

Oracle明確地支持READ COMMITTED(讀已提交)和SERIALIZABLE(可串行化)隔離級別
  不過,在Oracle中,READ COMMITTED則有得到讀一致查詢所需的所有屬性。另外,Oracle還秉承了READ UNCOMMITTED的“精神”。(有些數據庫)提供臟讀的目的是為了支持非阻塞讀,也就是說,查詢不會被同一個數據的更新所阻塞,也不會因為查詢而阻塞同一數據的更新。不過,Oracle不需要臟讀來達到這個目的,而且也不支持臟讀。但在其他數據庫中必須實現臟讀來提供非阻塞讀。
  除了4個已定義的SQL隔離級別外,Oracle還提供了另外一個級別,稱為READ ONLY(只讀)。READ ONLY事務相對於無法在SQL中完成任何修改的REPEATABLE READ或SERIALIZABLE事務。如果事務使用READ ONLY隔離級別,只能看到事務開始那一刻提交的修改,但是插入、更新和刪除不允許采用這種模式。如果使用這種模式,可以得到REPEATABLE READ和SERIALIZABLE級別的隔離性。
READ UNCOMMITTED
READ UNCOMMITTED隔離級別允許臟讀。Oracle沒有利用臟讀,甚至不允許臟讀。READ UNCOMMITTED隔離級別的根本目標是提供一個基於標準的定義以支持非阻塞讀。Oracle會默認地提供非阻塞讀,在數據庫中很難阻塞一個SELECT查詢。每個查詢都以一種讀一致的方式執行,而不論是SELECT、INSERT、UPDATE、MERGE,還是DELETE。這裏把UPDATE語句稱為查詢,UPDATE語句有兩個部分:一個是WHERE子句定義的讀部分,另一個是SET子句定義的寫部分。UPDATE語句會對數據庫進行讀寫,就像所有DML語句一樣。對此只有一個例外:使用VALUES子句的單行INSET是一個特例,因為這種語句沒有讀部分,而只有寫部分。

先創建表:

scott@ORCL>create table accounts
2 ( account_number number primary key,
3   account_balance number not null
4 );

插入數據:
  scott@ORCL>insert into accounts values(‘123‘,500);
已創建 1 行。
  scott@ORCL>insert into accounts values(‘456‘,240.25);
已創建 1 行。
  scott@ORCL>insert into accounts values(‘789‘,100);
已創建 1 行。
  scott@ORCL>commit;
提交完成。
數據如下:
  scott@ORCL>select * from accounts;

ACCOUNT_NUMBER ACCOUNT_BALANCE
-------------- ---------------
123 500
456 240.25
789 100

scott@ORCL>select sum(account_balance) from accounts;

SUM(ACCOUNT_BALANCE)
--------------------
840.25

下面,select語句開始執行,讀取第1行、第2行等。在查詢中的某一點上,一個事務將$400.00從賬戶123轉到賬戶789。這個事務完成了兩個更新,但是並沒有提交。現在數據如下:
行 帳號 賬戶金額是否?

1 123 ($500.00) changed to $100.00X
2 456 $240.25
3 789 ($100.00) changed to $500.00X


  如果我們執行的查詢要訪問某一塊,其中包含表“最後”已鎖定的行(第3行),則會註意到,這一行中的數據在開始執行之後有所改變。
為了提供一個一致(正確)的答案,Oracle在這個時刻會創建該塊的一個副本,其中包含查詢開始時行的“本來面目”。
它會讀取值$100.00,這就是查詢開始時該行的值。
Oracle就有效地繞過了已修改的數據,它沒有讀修改後的值,而是從undo段(回滾段)重新建立原數據。
因此可以返回一致而且正確的答案,而無需等待事務提交。

  再來看允許臟讀的數據庫,這些數據庫只會返回讀取那一刻在賬戶789中看到的值,在這裏就是$500.00。這個查詢會把轉賬的$400重復統計兩次。因此,它不僅會返回錯誤的答案,而且會返回表中根本不存在的一個總計(任何時間點都沒有這樣一個總計)。

  這裏的關鍵是,臟讀不是一個特性:而是一個缺點。Oracle中根本不需要臟讀。Oracle完全可以得到臟讀的所有好處(即無阻塞),而不會帶來任何不正確的結果。
READ COMMITTED
  事務只能讀取數據庫中已經提交的數據。這裏沒有臟讀,可能有不可重復讀(也就是說,在同一個事務中重復讀取同一行可能返回不同的答案)和幻像讀(與事務早期相比,查詢不光能看到已經提交的行,還可以看到新插入的行)。在數據庫應用中,READ COMMITTED可能是最常用的隔離級別了,這也是Oracle數據庫的默認模式。
根據先前的規則,在使用READ COMMITTED隔離級別的數據庫中執行的查詢肯定就會有相同的表現,真的是這樣嗎?這是不對的。
在Oracle中,由於使用多版本和讀一致查詢,無論是使用READ COMMITTED還是使用READ UNCOMMITTED,從ACCOUNTS查詢得到的答案總是一樣的。Oracle會按查詢開始時數據的樣子對已修改的數據進行重建,恢復其“本來面目”,因此會返回數據庫在查詢開始時的答案。
下面來看看其他數據庫,如果采用READ COMMITTED模式。我們從表所述的那個時間點開始:

1. 現在正處在表的中間。已經讀取並合計了前N行。
2. 另一個事務將$400.00從賬戶123轉到賬戶789。
3. 事務還沒有提交,所以包含賬戶123和789信息的行被鎖定。


我們知道Oracle中到達賬戶789那一行時會發生什麽,它會繞過已修改的數據,發現它原本是$100.00,然後完成工作。
如下顯示了其他數據庫(非Oracle)采用默認的READ COMMITTED模式運行時可能得到的答案:
非Oracle數據庫使用READ COMMITTED隔離級別時的時間表
時間 查詢 轉賬事務

T1 讀取第1行。到目前為止Sum=$500.00
T2 讀取第2行。到目前為止Sum=$740.25
T3 更新第1行,並對第1行加一個排他鎖,防止出現其他更新和讀取。
第1行現在的值為$100.00

T4 讀取第N行。Sum=…
T5 更新第3行,對這一行加一個排他鎖。
現在第3行的值是$500.00
T6 試圖讀取第3行,發現這一行被鎖定。
會話會阻塞,並等待這一行重新可用。
對這個查詢的所有處理都停止
T7 提交事務
T8 讀取第3行,發現值為$500.00,提供一個
最終答案,可惜這裏把$400.00重復計入了兩次

  首先要註意到,在這個數據庫中,到達賬戶789時, 我們的查詢會被阻塞。這個會話必須等待這一行,真正持有排它鎖的事務提交。這是因為這個原因,所以很多人養成一種壞習慣,在執行每條語句後都立即提交,而 不是處理一個合理的事務,其中包括將數據庫從一種一致狀態轉變為另一種一致狀態所需的所有語句。在大多數其他數據庫中,更新都會幹涉讀取。在這種情況下,我們不僅會讓用戶等待,而且他們苦苦等待的最後結果居然還是不正確的。我們會到數據庫中從來沒有過的答案,這就像臟讀一樣,但是與臟 讀不同的是,這一次還需要用戶等待這個錯誤的答案。

  不同的數據庫盡管采用相同的、顯然安全的隔離級別,而且在完全相同的環境中執行,仍有可能返回完全不同的答案。要知道的重要的一點是,在Oracle中,非阻塞讀並沒有以答案不正確作為代價。
REPEATABLE READ
  它不僅能給出一致的正確答案,還能避免丟失更新。
  1. 得到一致的答案
  如果隔離級別是REPEATABLE READ,從給定查詢得到的結果相對於某個時間點來說應該是一致的。大多數數據庫(不包括Oracle)都通過使用低級的共享讀鎖來實現可重復讀。共享讀鎖會防止其他會話修改我們已經讀取的數據。當然,這會降低並發性。Oracle則采用了更具並發性的多版本模型來提供讀一致的答案。
在Oracle中,通過使用多版本,得到的答案相對於查詢開始執行那個時間點是一致的。在其他數據庫中,通過使用共享讀鎖,可以得到相對於查詢完成那個時間點一致的答案,也就是說,查詢結果相對於我們得到的答案的那一刻是一致的。
在一個采用共享讀鎖來提供可重復讀的系統中,可以觀察到,查詢處理表中的行時,這些行都會鎖定。
非Oracle數據庫使用READ REPEATABLE隔離級別時的時間表1
時間 查詢 轉賬事務

T1 讀取第1行。到目前為止Sum=$500.00。
塊1上有一個共享讀鎖
T2 讀取第2行。到目前為止Sum=$740.25。
塊2上有一個共享讀鎖
T3 試圖更新第1行,但是被阻塞。
這個事務被掛起,直至可以得到一個排他鎖
T4 讀取第N行。Sum=…
T5 讀取第3行,看到$100.00,提供最後的答案
T6 提交事務
T7 更新第1行,並對這一塊加一個排他鎖。
現在第1行有$100.00
T8 更新第3行,對這一塊加一個排它鎖。
第3行現在的值為$500.00。
提交事務


現在我們得到了正確的答案,但是這是有代價的:需要物理地阻塞一個事務,並且順序執行兩個事務。這是使用共享讀鎖來得到一致答案的副作用之一:數據的讀取器會阻塞數據的寫入器。不僅如此,在這些系統,數據的寫入器還會阻塞數據讀取器。

由此可以看到,共享讀鎖會妨礙並發性,而且還導致有欺騙性的錯誤。在下表中,我們先從原來的表開始,不過這一次的目標是把$50.00從賬戶789轉賬到賬戶123。
非Oracle數據庫使用READ REPEATABLE隔離級別時的時間表2
時間 查詢 轉賬事務

T1 讀取第1行。到目前為止Sum=$500.00。
塊1上有一個共享讀鎖
T2 讀取第2行。到目前為止Sum=$740.25。
塊2上有一個共享讀鎖
T3 更新第3行,對塊3加一個排他鎖,防止出現其他更新和共享讀鎖。
現在這一行的值為$50.00
T4 讀取第N行。Sum=…
T5 試圖更新第1行,但是被阻塞。
這個事務被掛起,直至可以得到一個排他鎖
T6 試圖讀取第3行,但是做不到,
因為該行已經有一個排他鎖

  我 們陷入了經典的死鎖條件。我們的查詢擁有更新需要的資源,而更新也持有著查詢所需的資源。查詢與更新事務陷入死鎖。要把其中一個作為犧牲品,將其中止。這 樣說來,我們可能會花大量的時間和資源,而最終只是會失敗並回滾。這是共享讀鎖的另一個副作用:數據的讀取器和寫入器可能而且經常相互死鎖。
Oracle中可以得到語句級的讀一致性,而不會帶來讀阻塞寫的現象,也不會導致死鎖。Oracle從不使用共享讀鎖,從來不會。Oracle選擇了多版本機制,盡管更難實現,但絕對更具並發性。
  2. 丟失更新:另一個可移植性問題
  在采用共享讀鎖的數據庫中,REPEATABLE READ的一個常見用途是防止丟失更新。

  在一個采用共享讀鎖(而不是多版本)的數據庫中,如果啟用了REPEATABLE READ,則不會發生丟失更新錯誤。這些數據庫中之所以不會發生丟失更新,原因是:這樣選擇數據就會在上面加一個鎖,數據一旦由一個事務讀取,就不能被任何其他事務修改。
盡管聽上去使用共享讀鎖好像不錯,但如果讀取數據時在所有數據上都加共享讀鎖,這肯定會嚴重地限制並發讀和修改。所以,盡管在這些數據庫中這個隔離級別可以防止丟失更新,但是與此同時,也使得完成並發操作的能力化為烏有!
SEAIALIZABLE
  一般認為這是最受限的隔離級別,但是它也提供了最高程度的隔離性。SERIALIZABLE事務在一個環境中操作時,就好像沒有別的用戶在修改數據庫中的數據一樣。我們讀取的所有行在重新讀取時都肯定完全一樣,所執行的查詢在整個事務期間也總能返回相同的結果。例如,如果執行以下查詢:
Select * from T;
Begin dbms_lock.sleep( 60*60*24 ); end;
Select * from T;
  從T返回的答案總是相同的,就算是我們睡眠了24小時也一樣(或者會得到一個ORA-1555:snapshot too old錯誤)。這個隔離級別可以確保這兩個查詢總會返回相同的結果。其他事務的副作用(修改)對查詢是不可見的,而不論這個查詢運行了多長時間。
Oracle中是這樣實現SERIALIZABLE事務的:原本通常在語句級得到的讀一致性現在可以擴展到事務級。
註意 前面提到過,Oracle中還有一種稱為READ ONLY的隔離級別。它有著SERIALIZABLE隔離級別的所有性質,另外還會限制修改。需要指出,SYS用戶(或作為SYSDBA連接的用戶)不能有READ ONLY或SERIALIZABLE事務。在這方面,SYS很特殊。

結果並非相對於語句開始的那個時間點一致,而是在事務開始的那一刻就固定了。
Oracle使用回滾段按事務開始時數據的原樣來重建數據,而不是按語句開始時的樣子重建。
這種隔離性是有代價的,可能會得到以下錯誤:

ERROR at line 1:
ORA-08177: cant serialize access for this transaction

只要你試圖更新某一行,而這一行自事務開始後已經修改,你就會得到這個消息。
註意Oracle試圖完全在行級得到這種隔離性,但是即使你想修改的行尚未被別人修改後,也可能得到一個ORA-01877錯誤。發生ORA-01877錯誤的原因可能是:包含這一行的塊上有其他行正在被修改。
  Oracle采用了一種樂觀的方法來實現串行化,它認為你的事務想要更新的數據不會被其他事務所更新,而且把寶押在這上面。一般確實是這樣的,特別是在事務執行得很快的OLTP型系統中。盡管在其他系統中這個隔離級別通常會降低並發性,但是在Oracle中,倘若你的事務在執行期間沒有別人更新你的數據,則能提供同等程度的並發性,就好像沒有SERIALIZABLE事務一樣。另一方面,這也是有缺點的,如果寶押錯了,你就會得到ORA_08177錯誤。不過,可以再想想看,冒這個險還是值得的。如果你確實要更新信息,就應該使用SELECT … FOR UPDATE,這會實現串行訪問。所以,如果使用SERIALIZABLE隔離級別,只要保證以下幾點就能很有成效:

1. 一般沒有其他人修改相同的數據
2. 需要事務級讀一致性
3. 事務都很短(這有助於保證第一點)


  Oracle發現這種方法的可擴縮性很好,足以運行其所有TPC-C(這是一個行業標準OLTP基準)。在許多其他的實現中,你會發現這種隔離性都是利用共享讀鎖達到的,相應地會帶來死鎖和阻塞。而在Oracle中,沒有任何阻塞,但是如果其他會話修改了我們也想修改的數據,則會得到ORA-08177錯誤。
要記住,如果隔離級別設置為SERIALIZABEL,事務開始之後,你不會看到數據庫中做出任何修改,直到提交事務為止。如果應用試圖保證其數據完整性約束,如資源調度程序,就必須在這方面特別小心。我們無法保證一個多用戶系統中的完整性約束,因為我們看不到其他未提交會話做出的修改。通過使用SERIALIZABLE。這些未提交的修改還是看不到,但是同樣也看不到事務開始後執行的已提交的修改!

  SERIALIZABLE並不意味著用戶執行的所有事務都表現得好像是以一種串行化方式一個接一個地執行。SERIALIZABLE不代表事務有某種串行順序並且總能得到相同的結果。盡管按照SQL標準來說,這種模式不允許前面所述的幾種現象,但不能保證事務總按串行方式順序執行。
sys@ORCL>create table a ( x int );
表已創建。

sys@ORCL>create table b ( x int );
表已創建。
表7-7 SERIALIZABLE事務例子
時間 會話1執行 會話2執行

T1 Alter session set isolation_level=serializable;

T2 Alter session set isolation_level=serializable;

T3 Insert into a select count(*) from b;

T4 Insert into b select count(*) from a;
T5 Commit;
T6 Commit;

  現在,一起都完成後,表A和B中都有一個值為0的行。如果事務有某種“串行”順序,就不可能得到兩個都包含0值的表。如果會話1在會話2之前執行,表B就會有一個值為1的行。如果會話2在會話1之前執行,那麽表A則有一個值為1的行。不過,按照這裏的執行方式,兩個表中的行都有值0,不論是哪個事務,執行時就好像是此時數據庫中只有它一個事務一樣。不管會話1查詢多少次表B,計數(count)都是對T1時間數據庫中已提交記錄的計數。不論會話2查詢多少次表A,都會得到與T2時間相同的計數。

READ ONLY
  READ ONLY事務與SERIALIZABLE事務很相似,惟一的區別是READ ONLY事務不允許修改,因此不會遭遇ORA-08177錯誤。READ ONLY事務的目的是支持報告需求,即相對於某個時間點,報告的內容應該是一致的。在Oracle中,采用這種模式,如果一個報告使用50條SELECT語句來收集數據,所生成的結果相對於某個時間點就是一致的,即事務開始的那個時間點。你可以做到這一點,而無需在任何地方鎖定數據。
為達到這個目標,就像對單語句一樣,也使用了同樣的多版本機制。會根據需要從回滾段重新創建數據,並提供報告開始時數據的原樣。不過,READ ONLY事務也不是沒有問題。在SERIALIZABLE事務中你可能會遇到ORA-08177錯誤,而在READ ONLY事務中可能會看到ORA-1555:snapshot too old錯誤。如果系統上有人正在修改你讀取的數據,就會發生這種情況。對這個信息所做的修改(undo信息)將記錄在回滾段中。但是回滾段以一種循環方式使用,這與重做日誌非常相似。報告運行的時間越長,重建數據所需的undo信息就越有可能已經不在那裏了。回滾段會回繞,你需要的那部分回滾段可能已經被另外某個事務占用了。此時,就會得到ORA-1555錯誤,只能從頭再來。
惟一的解決方案就是為系統適當地確定回滾段的大小。問題在於,回滾段是完成數據庫工作的一個關鍵組件,除非有合適的大小,否則就會遭遇這個錯誤。如果真的遇到了這個錯誤,這就說明你沒有正確地設置回滾段的大小,需要適當地加以修正。
多版本讀一致性的含義
多版本 不僅能提供一致(正確)的答案,還有高度的並發性。
一種會失敗的常用數據倉庫技術
許多人都喜歡用這樣一種常用的數據倉庫技術:

1) 他們使用一個觸發器維護源表中的一個LAST_UPDATED列。
(2) 最初要填充數據倉庫表時,他們要記住當前的時間,為此會選擇源系統上的SYSDATE。例如,假設現在剛好是上午9:00。
(3) 然後他們從事務系統中拉(pull)出所有行,這是一個完整的SELECT * FROM TABLE查詢,可以得到最初填充的數據倉庫。
(4) 要刷新這個數據倉庫,他們要再次記住現在的時間。例如,假設已經過去了1個小時,現在源系統上的時間就是10:00.他們會記住這一點。然後拉出自上午9:00(也就是第一次拉出數據之前的那個時刻)以來修改過的所有記錄,並把這些修改合並到數據倉庫中。

  註意 這種技術可能會在兩次連續的刷新中將相同的記錄“拉出”兩次。由於時鐘的粒度所致,這是不可避免的。MERGE操作不會受此影響(即更新數據倉庫中現有的記錄,或插入一個新記錄)。

  例如,假設在上午8:59:30時,這個事務已經更新了表中我們想復制的一行。在上午9:00, 開始拉數據時,會讀取這個表中的數據,但是我們看不到對這一行做的修改;只能看到它的最後一個已提交的版本。如果在查詢中到達這一行時它已經鎖定,我們就 會繞過這個鎖。如果在到達它之前事務已經提交,我們還是會繞過它讀取查詢開始時的數據,因為讀一致性只允許我們讀取語句開始時數據庫中已經提交的數據。在 上午9:00第一次拉數據期間我們讀不到這一行的新版本,在上午10:00刷新期間也讀不到這個修改過的行。為什麽呢?上午10:00的刷新只會拉出自那天早上上午9:00以後修改的記錄,但是這個記錄是在上午8:59:30時修改的,我們永遠也拉不到這個已修改的記錄。

  在許多其他的數據庫中,其中讀會被寫阻塞,可以完成已提交但不一致的讀,那麽這個刷新過程就能很好地工作。如果上午9:00(第一次拉數據時)我們到達這一行,它已經上鎖,我們就會阻塞,等待這一行可用,然後讀取已提交的版本。如果這一行未鎖定,那麽只需讀取就行,因為它們都是已提交的。

  那麽,這是否意味著前面的邏輯就根本不能用呢?也不是,這只是說明我們需要用稍微不同的方式來得到“現在”的時間。應該查詢V$TRANSACTION,找出最早的當前時間是什麽,以及這個視圖中START_TIME列記錄的時間。我們需要拉出自最老事務開始時間(如果沒有活動事務,則取當前的SYSDATE值)以來經過修改的所有記錄:

scott@ORCL>select nvl( min(to_date(start_time,mm/dd/rr hh24:mi:ss)),sysdate)
2 from v$transaction;

NVL(MIN(TO_DAT
--------------
06-5月 -18


  在這個例子中,這就是上午8:59:30,即修改這一行的事務開始的那個時間。我們在上午10:00刷新數據時,會拉出自那個時間以來發生的所有修改,把這些修改合並到數據倉庫中,這就能得到需要的所有東西
解釋熱表上超出期望的I/O


  生產環境中在一個大負載條件下,一個查詢使用的I/O比你在測試或開發系統時觀察到的I/O要多得多,而你無法解釋這一現象。你查看查詢執行的I/O時,註意到它比你在開發系統中看到的I/O次數要多得多。然後,你再在測試環境中恢復這個實例,卻發現I/O又 降下來了。但是到了生產環境中,它又變得非常高(但是好像還有些變化:有時高,有時低,有時則處於中間)。可以看到,造成這種現象的原因是:在你測試系統 中,由於它是獨立的,所以不必撤銷事務修改。不過,在生產系統中,讀一個給定的塊時,可能必須撤銷(回滾)多個事務所做的修改,而且每個回滾都可能涉及I/O來獲取undo信息並應用於系統。
可能只是要查詢一個表,但是這個表上發生了多個並發修改,因此你看到Oracle正在讀undo段,從而將塊恢復到查詢開始時的樣子。

scott@ORCL>create table t ( x int );
表已創建。

scott@ORCL>insert into t values ( 1 );
已創建 1 行。

scott@ORCL>exec dbms_stats.gather_table_stats( user, T );
PL/SQL 過程已成功完成。

scott@ORCL>select * from t;

X
----------
1

下面,將會話設置為使用SERIALIZABLE隔離級別,這樣無論在會話中運行多少次查詢,都將得到事務開始時刻的查詢結果:

  

scott@ORCL>alter session set isolation_level=serializable;

會話已更改。

下面查詢這個小表,並觀察執行的I/O次數:

scott@ORCL>set autotrace on statistics
scott@ORCL>select * from t;

X
----------
1

統計信息

----------------------------------------------------------

0 recursive calls
0 db block gets
7 consistent gets
0 physical reads
0 redo size
524 bytes sent via SQL*Net to client
520 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

由此可見,完成這個查詢用了7個I/O(一致獲取,consistent get)。在另一個會話中,我們將反復修改這個表:

scott@ORCL>begin
2 for i in 1 .. 10000
3 loop
4 update t set x = x+1;
5 commit;
6 end loop;
7 end;
8 /


PL/SQL 過程已成功完成。
再返回到前面的SERIALIZABLE會話,重新運行同樣的查詢:

scott@ORCL>select * from t;

X
----------
1

統計信息

----------------------------------------------------------

0 recursive calls
0 db block gets
10002 consistent gets
0 physical reads
0 redo size
524 bytes sent via SQL*Net to client
520 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

  這一次執行了10,002次I/O,簡直有天壤之別。那麽,所有這些I/O是從哪裏來的呢?這是因為Oracle回滾了對該數據庫塊的修改。在運行第二個查詢時,Oracle知道查詢獲取和處理的所有塊都必須針對事務開始的那個時刻。到達緩沖區緩存時,我們發現,緩存中的塊“太新了”,另一個會話已經把這個塊修改了10,000次。查詢無法看到這些修改,所以它開始查找undo信息,並撤銷上一次所做的修改。它發現這個回滾塊還是太新了,然後再對它做一次回滾。這個工作會復發進行,直至最後發現事務開始時的那個版本(即事務開始時數據庫中的已提交塊)。這才是我們可以使用的塊,而且我們用的就是這個塊。

  註意 如果你想再次運行SELECT * FROM T,可能會看到I/O再次下降到3;不再是10,002。為什麽呢?Oracle能把同一個塊的多個版本保存在緩沖區緩存中。你撤銷對這個塊的修改時,也就把相應的版本留在緩存中了,這樣以後執行查詢時就可以直接訪問。
那麽,是不是只在使用SERIALIZABLE隔離級別時才會遇到這個問題呢?不,絕對不是。可以考慮一個運行5分鐘的查詢。在查詢運行的這5分鐘期間,它從緩沖區緩存獲取塊。每次從緩沖區緩存獲取一個塊時,都會完成這樣一個檢查:“這個塊是不是太新了?如果是,就將其回滾。”另外,要記住,查詢運行的時間越長,它需要的塊在此期間被修改的可能性就越大。
  現在,數據庫希望進行這個檢查(也就是說,查看塊是不是“太新”,並相應地回滾修改)。正是由於這個原因,緩沖區緩存實際上可能在內存中包含同一個塊的多個版本。通過這種方式,很有可能你需要的版本就在緩存中,已經準備好,正等著你使用,而無需使用undo信息進行物化。請看以下查詢:

scott@ORCL>select file#, block#, count(*)
2 from v$bh
3 group by file#, block#
4 having count(*) > 3
5 order by 3
6 /

FILE# BLOCK# COUNT(*)
---------- ---------- ----------
2 4283 4
2 30659 5
2 65412 5
2 30958 5
2 65413 6
4 149 6
2 65415 6
4 3725 6
2 30707 6
2 65414 6

已選擇10行。

統計信息
----------------------------------------------------------

417 recursive calls
0 db block gets
83 consistent gets
3 physical reads
0 redo size
824 bytes sent via SQL*Net to client
520 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
11 sorts (memory)
0 sorts (disk)
10 rows processed

  可以用這個查詢查看這些塊。一般而言,任何時間點上緩存中一個塊的版本大約不超過6個,但是這些版本可以由需要它們的任何查詢使用。

通常就是這些小的“熱表”會因為讀一致性遭遇I/O膨脹問題。另外,如果查詢需要針對易失表長時間運行,也經常受到這個問題的影響。運行的時間越長,“它們也就會運行得越久”,因為過一段時間,它們可能必須完成更多的工作才能從緩沖區緩存中獲取一個塊。
寫一致性
到此為止,我們語句了解了讀一致性:Oracle可以使用undo信息來提供非阻塞的查詢和一致(正確)的讀。我們了解到,查詢時,Oracle會從緩沖區緩存中讀出塊,它能保證這個塊版本足夠“舊”,能夠被該查詢看到。
但是,這又帶來了以下的問題:寫/修改會怎麽樣呢?如果運行以下UPDATE語句,會發生什麽:
update t set x = 2 where x = 5;
在該語句運行時,有人將這條語句已經讀取的一行從x=5更新為x=6,並提交,如果是這樣會發生什麽情況?也就是說,在UPDATE開始時,某一行有值x=5。在UPDATE使用一致讀來讀取表時,它看到了UPDATE開始時這一行是x=5。但是,現在x的當前值是6,不再是5了,在更新X的值之前,Oracle會查看x是否還是5。現在會發生什麽呢?這會對更新有什麽影響?
顯然,我們不能修改塊的老版本,修改一行時,必須修改該塊的當前版本。另外,Oracle無法簡單地跳過這一行,因為這將是不一致讀,而且是不可預測的。在這種情況下,我們發現Oracle會從頭重新開始寫修改。
1一致讀和當前讀
Oracle處理修改語句時會完成兩類塊獲取。它會執行:
1. 一致讀(Consistent read):“發現”要修改的行時,所完成的獲取就是一致讀。
2. 當前讀(Current read):得到塊來實際更新所要修改的行時,所完成的獲取就是當前讀。
使用TKPROF可以很容易地看到這一點:
scott@ORCL>alter session set sql_trace=true;
會話已更改。
scott@ORCL>alter system set timed_statistics=true;
系統已更改。
scott@ORCL>show parameter sql_trace
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
sql_trace boolean TRUE
scott@ORCL>SELECT VALUE FROM V$PARAMETER WHERE NAME = ‘user_dump_dest‘ ;
VALUE
----------------------------------------
d:\app\administrator\diag\rdbms\orcl\orcl\trace
scott@ORCL>select * from t; X---------- 10003scott@ORCL>update t t1 set x = x+1;已更新 1 行。scott@ORCL>update t t2 set x = x+1;已更新 1 行。scott@ORCL>select * from t ; X---------- 10005


運行TKPROF並查看結果時,可以看到如下的結果:

C:\Users\Administrator>tkprof D:\app\Administrator\diag\rdbms\orcl\orcl\trace\orcl_ora_5604.trc D:\trace.txt print=100 record=sql.txt sys=no

TKPROF: Release 11.2.0.1.0 - Development on 星期日 5月 6 14:04:38 2018

Copyright (c) 1982, 2009, Oracle and/or its affiliates. All rights reserved.
select * from t

call count query current rows
------- ------ ---------- ---------- ----------
Parse 1 0 0 0
Execute 1 0 0 0
Fetch 2 3 0 1
------- ------ ---------- ---------- ----------
total 4 3 0 1

update t t1 set x = x+1

call count query current rows
------- ------ ---------- ---------- ----------
Parse 1 0 0 0
Execute 1 3 3 1
Fetch 0 0 0 0
------- ------ ---------- ---------- ----------
total 2 3 3 1

update t t2 set x = x+1

call count query current rows
------- ------ -------- ---------- ----------
Parse 3 0 0 0
Execute 3 21 3 3
Fetch 0 0 0 0
------- ------ ---------- ---------- ----------
total 6 21 3 3
因此,在一個正常的查詢中,我們會遇到3個查詢模式獲取(一致模式獲取,query(consistent)mode get)。在第一個UPDATE期間,會遇到同樣的3個當前模式獲取(current mode get)。完成這些當前模式獲取是為了分別得到現在的表塊(table block),也就是包含待修改行的塊;得到一個undo段塊(undo segment block)來開始事務;以及一個undo塊(undo block)。第二個更新只有一個當前模式獲取,因為我們不必再次完成撤銷工作,只是要利用一個當前獲取來得到包含待更新行的塊。既然存在當前模式獲取,這就什麽發生了某種修改。在Oracle用新信息修改一個塊之前,它必須得到這個塊的當前副本。

那麽,讀一致性對修改有什麽影響呢?這麽說吧,想像一下你正在對某個數據庫表執行以下UPDATE語句:

我們知道,查詢的WHERE Y=5部分(即讀一致階段)會使用一個一致讀來處理(TKPROF報告中的查詢模式獲取)。這個語句開始執行時表中已提交的WHERE Y=5記錄集就是它將看到的記錄(假設使用READ COMMITED隔離級別,如果隔離級別是SERIALIZABLE,所看到的則是事務開始是存在的WHERE Y=5記錄集)。這說明,如果UPDATE語句從開始到結束要花5分鐘來進行處理,而有人在此期間向表中增加並提交了一個新記錄,其Y列值為5,那麽UPDATE看不到這個記錄,因為一致讀是看不到新記錄的。這在預料之中,也是正常的。但問題是,如果兩個會話按順序執行以下語句會發生什麽情況呢?

Update t set y = 10 where y = 5;
Update t Set x = x+1 Where y = 5;
表7-8 更新序列

時間 會話1
會話2 註釋

T1 Update t
這會更新與條件匹配的一行

set y = 10

where y = 5;

T2 Update t 使用一致讀,這會找到會話1修改的記錄,

但是無法更新這個記錄,因為會話1已經將其阻塞。

會話2將被阻塞,並等待這一行可用

set x = x+1

where y = 5;

T3 Commit; 這會解放會話2;會話2不再阻塞。他終於可以在

包含這一行(會話1開始更新時Y等於5的那一行)

的塊上完成當前讀

因此開始UPDATE時Y=5的記錄不再是Y=5了。UPDATE的一致讀部分指出:“你想更新這個記錄,因為我們開始時Y是5”,但是根據塊的當前版本,你會這樣想:”噢,不行,我不能更新這一行,因為Y不再是5了,這可能不對。“

如果我們此時只是跳過這個記錄,並將其忽略,就會有一個不確定的更新。這可能會破壞數據一致性和完整性。更新的結果(即修改了多少行,以及修改了哪些行)將 取決於以何種順序命中(找到)表中的行以及此時剛好在做什麽活動。在兩個不同的數據庫中,取同樣的一個行集,每個數據庫都以相同的順序運行事務,可能會觀 察到不同的結果,這只是因為這些行在磁盤上的位置不同。

在這種情況下,Oracle會選擇重啟動更新。如果開始時Y=5的行現在包含值Y=10,Oracle會悄悄地回滾更新,並重啟動(假設使用的是READ COMMITTED隔離級別)。如果你使用了SERIALIZABLE隔離級別,此時這個事務就會收到一個ORA-08177: can’t serialize access錯誤。采用READ COMMITTED模式,事務回滾你的更新後,數據庫會重啟動更新(也就是說,修改更新相關的時間點),而且它並非重新更新數據,而是進入SELECT FOR
UPDATE模式,並試圖為你的會話鎖住所有WHERE Y=5的行。一旦完成了這個鎖定,它會對這些鎖定的數據運行UPDATE,這樣可以確保這一次就能完成而不必(再次)重啟動。

但是再想想“會發生什麽……“,如果重啟動更新,並進入SELECT FOR UPDATE模式(與UPDATE一樣,同樣有讀一致塊獲取(read-consistent block get)和讀當前塊獲取(read current block get)),開始SELECT FOR UPDATE時Y=5的一行等到你得到它的當前版本時卻發現Y=11,會發生什麽呢?SELECT FOR UPDATE會重啟動,而且這個循環會再來一遍。

查看重啟動

scott@ORCL>create table t ( x int, y int );
表已創建。

scott@ORCL>insert into t values ( 1, 1 );
已創建 1 行。

scott@ORCL>commit;
提交完成。
為了觀察重啟動,只需要一個觸發器打印出一些信息。我們會使用一個BEFORE UPDATE FOR EACH ROW觸發器打印出行的前映像和作為更新結果的後映像:
scott@ORCL>create or replace trigger t_bufer
2 before update on t for each row
3 begin
4 dbms_output.put_line
5 ( ‘old.x = ‘ || :old.x ||
6 ‘, old.y = ‘ || :old.y );
7 dbms_output.put_line
8 ( ‘new.x = ‘ || :new.x ||
9 ‘, new.y = ‘ || :new.y );
10 end;
11 /

觸發器已創建
下面可以更新這一行:
scott@ORCL>set serveroutput on
scott@ORCL>update t set x = x+1;
old.x = 1, old.y = 1
new.x = 2, new.y = 1

已更新 1 行。
到此為止,一切都不出所料:觸發器每觸發一次,我們都可以看到舊值和新值。不過,要註意,此時還沒有提交,這一行仍被鎖定。在另一個會話中,執行以下更新:
scott@ORCL>set serveroutput on
scott@ORCL>update t set x = x+1 where x > 0;
立即阻塞,因為第一個會話將這一行鎖住了。如果現在回到第一個會話,並提交,
scott@ORCL>commit;
會看到第二個會話中有以下輸出:
scott@ORCL>update t set x = x+1 where x > 0;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
old.x = 2, old.y = 1
new.x = 3, new.y = 1

已更新 1 行。
可以看到,行觸發器看到這一行有兩個版本。行觸發器會觸發兩次:一次提供了行原來的版本以及我們想把原來這個版本修改成什麽,另一次提供了最後實際更新的行。由於這是一個BEFORE FOR EACH ROW觸發器,Oracle看到了記錄的讀一致版本,以及我們想對它做的修改。不過,Oracle以當前模式獲取塊,從而在BEFORE FOR EACH ROW觸發器觸發之後具體執行更新。它會等待觸發器觸發後再以當前模式得到塊,因為觸發器可能會修改:NEW值。因此Oracle在觸發器執行之前無法修改這個塊,而且觸發器的執行可能要花很長時間。由於一次只有一個會話能以當前模式持有一個塊;所以Oracle需要對處於當前模式下的時間加以限制。
觸發器觸發後,Oracle以當前模式獲取這個塊,並註意到用來查找這一行的X列已經修改過。由於使用了X來定位這條記錄,而且X已經修改,所以數據庫決定重啟動查詢。註意,盡管X從1更新到2,但這並不會使該行不滿足條件(X>0);這條UPDATE語句還是會更新這一行。而且,由於X用於定位這一行,而X的一致讀值(這裏是1)不同於X的當前模式讀值(2),所以在重啟動查詢時,觸發器把值X=2(被另一個會話修改之後)看作是:OLD值,而把X=3看作是:NEW值。
由此就可以看出發生了重啟動。要用觸發器查看實際的情況;否則,重啟動一般是“不可檢測的“。例如更新多行時,發現某一行會導致重啟動,而導致一個很大的UPDATE語句回滾工作,這也可能是一個癥狀,但是很難明確地指出”這個癥狀是重啟動造成的“。
即使語句本身不一定導致重啟動,觸發器本身也可能導致發生重啟動。一般來講,UPDATE或DELETE語句的WHERE子句中引用的列能用於確定修改是否需要重啟動。Oracle使用這些列完成一個一致讀,然後以當前模式獲取塊時,如果檢測到任何列有修改,就會重啟動這個語句。一般來講,不會檢查行中的其他列。例如,下面重新運行前面的例子,這裏使用WHERE Y>0來查找行:
scott@ORCL>update t set x = x+1 where y > 0;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
old.x = 2, old.y = 1
new.x = 3, new.y = 1

已更新 1 行。
你開始可能會奇怪,“查看Y值時,Oracle為什麽會把觸發器觸發兩次?它會檢查整個行嗎?“從輸出結果可以看到,盡管我們在搜索Y>0,而且根本沒有修改Y,但是更新確實重啟動了,觸發器又觸發了兩次。不過,倘若重新創建觸發器,只打印出它已觸發這個事實就行了,而不再引用:OLD和:NEW值:
scott@ORCL>create or replace trigger t_bufer
2 before update on t for each row
3 begin
4 dbms_output.put_line( ‘fired‘ );
5 end;
6 /

觸發器已創建

scott@ORCL>update t set x = x+1;
fired

已更新 1 行。
再到第二個會話中,運行更新後,可以觀察到它會阻塞。提交阻塞會話(即第一個會話)後,可以看到以下輸出:
scott@ORCL>update t set x = x+1 where y > 0;
fired

已更新 1 行。
這一次觸發器只觸發了一次,而不是兩次。這說明,:NEW和:OLD列值在觸發器中引用時,也會被Oracle用於完成重啟動檢查。在觸發器中引用:NEW.X和:OLD.X時,會比較X的一致讀值和當前讀值,並發現二者不同。這就會帶來一個重啟動。從觸發器將這一列的引用去掉後,就沒有重啟動了。
所以,對此的原則是:WHERE子句中查找行所用的列集會與行觸發器中引用的列進行比較。行的一致讀版本會與行的當前讀版本比較,只要有不同,就會重啟動修改。
註意 使用AFTER FOR EACH ROW觸發器比使用BEFORE FOR EACH ROW更高效。AFTER觸發器不存在這些問題。

為什麽重啟動對我們很重要?
首先應該註意到“我們的觸發器觸發了兩次!“表中只有一行,而且只有一個BEFORE FOR EACH ROW觸發器。我們更新了一行,但觸發器卻觸發了兩次。
想想看這會有什麽潛在的影響。如果你有一個觸發器會做一些非事務性的事情,這可能就是一個相當嚴重的問題。例如,考慮這樣一個觸發器,它要發出一個更新(電 子郵件),電子郵件的正文是“這是數據以前的樣子,它已經修改成現在這個樣子“。如果從觸發器直接發送這個電子郵件,(在Oracle9i中使用UTL_SMTY,或者在Oracle 10g及以上版本中使用UTL_MAIL),用戶就會收到兩個電子郵件,而且其中一個報告的更新從未實際發生過。
如果在觸發器中做任何非事務性的工作,就會受到重啟動的影響。考慮以下影響:
1.考慮一個觸發器,它維護著一些PL/SQL全局變量,如所處理的個數。重啟動的語句回滾時,對PL/SQL變量的修改不會“回滾“。
2.一般認為,以UTL_開頭的幾乎所有函數(UTL_FILE、UTL_HTTP、UTL_SMTP等)都會受到語句重啟動的影響。語句重啟動時,UTL_FILE不會“取消“對所寫文件的寫操作。
3.作為自治事務一部分的觸發器肯定會受到影響。語句重啟動並回滾時,自治事務無法回滾。
所有這些後果都要小心處理,要想到對於每一個觸發器可能會觸發多次,或者甚至對根本未被語句更新的行也會觸發。
之所以要當心可能的重啟動,還有一個原因,這與性能有關。我們一直在使用單行的例子,但是如果你開始一個很大的批更新,而且它處理了前100,000條記錄後重啟動了會怎麽樣?它會回滾前100,000行的修改,以SELECT FOR UPDATE模式重啟動,在此之後再完成那100,000行的修改。
你可能註意到了,放上那個簡單的審計跟蹤觸發器後(即讀取:NEW和:OLD值的觸發器),即使除了增加了這些新觸發器外什麽也沒有改變,但性能可能會突然差到你無法解釋的地步。你可能在重啟動過去從未用過的查詢。或者你增加了一個小程序,它只更新某處的一行,確使過去只運行1個小時的批處理突然需要幾個小時才能運行完,其原因只是重啟動了過去從未發生過工作。
---------------------

原文:https://blog.csdn.net/A0001AA/article/details/80110803

oracle 並發與多版本