(二)數據庫事務與隔離——示例

 本示例文檔演示SQL SERVER,ORACLE 下不同事務隔離級別的區別,以及兩種數據庫本身的特點
 爲了模擬併發環境,SQL SERVER在SMO程序中打開兩個查詢窗口即可。oracle可以用兩個sql *plus程序連接到相同數據庫來模擬
    SQL SERVER、ORACLE中兩個併發用戶用事務1,事務2簡稱。
    所有測試例子,都以最初測試表腳本運行後狀態爲基準。
    在下列例子中,set transaction isolation level語句會改變會話的隔離級別,直到會話結束。故測試完畢需要改回默認級別。
    最後,但並不是最不重要。以下的演示和相關解釋,都是基於易於理解的原則來的,實際的情況可能更復雜,但對開發人員來說,理解如此程度的簡化模型已經足夠了。

測試表腳本:
SQL SERVER
CREATE TABLE [Customer](
    [CustID] [int] NOT NULL,
    [Fname] [nvarchar](20),
    [Lname] [nvarchar](20),
    [Address] [nvarchar](50),
    [City] [nvarchar](20),
    [State] [nchar](2) DEFAULT ('CA'),
    [Zip] [nchar](5) NOT NULL,
    [Phone] [nchar](10)
)
insert into customer values(1, 'Gary', 'Mckee', '111 Main', 'Palm Springs', 'CA', 94312, 7605551212)
insert into customer values(2, 'Tom', 'Smith', '609 Geogia', 'Fresno' 'JP', 33045, 5105551212)
insert into customer values(3, 'Jams', 'bond', 'ST Geogie 21', 'Washington', 'NY', 20331, 4405551864)

ORACLE
CREATE TABLE Customer(
    CustID int NOT NULL,
    Fname nvarchar2(20),
    Lname nvarchar2(20),
    Address nvarchar2(50),
    City nvarchar2(20),
    State nchar(2) DEFAULT 'CA',
    Zip nchar(5) NOT NULL,
    Phone nchar(10)
);
insert into customer values(1, 'Gary', 'Mckee', '111 Main', 'Palm Springs', 'CA', 94312, 7605551212);
insert into customer values(2, 'Tom', 'Smith', '609 Geogia', 'Fresno', 'JP', 33045, 5105551212);
insert into customer values(3, 'Jams', 'bond', 'ST Geogie 21', 'Washington', 'NY', 20331, 4405551864);


1。Sqlserver與oracle單條語句處理對比
SQL SERVER單條語句默認自動提交,即單條語句自動作爲一個事務處理;而oracle的原則是儘量延後提交,除非遇到顯式提交命令或者DDL語句。
SQL SERVER
打開事務1:
運行:select * from customer
可以看到表有3條記錄
運行:insert into customer values(4, 'Hello', 'world', 'paradise road 01', 'heaven', 'XY', 00001, 1234564321)
轉到事務2:
運行:select * from customer
可以看到事務1中剛插入的custid爲4的記錄。

ORACLE
打開事務1,運行:
select * from customer;
可以看到表有3條記錄,運行:
insert into customer values(4, 'Hello', 'world', 'paradise road 01', 'heaven', 'XY', 00001, 1234564321);
轉到事務2,運行:
select * from customer;
能看到的還是3條記錄,事務1中剛插入的一條記錄未自動提交,看不到。
轉到事務1,運行:
commit;
轉到事務2,運行:
select * from customer;
現在能看到4條記錄了。


2. 丟失更新
Sqlserver完全兼容ANSI 92標準定義的4個隔離級別。它的默認隔離級別是提交讀(read committed),在該級別下,可能會有丟失更新的問題。Oracle的默認情形也一樣。故不再重複。
SQL SERVER
打開事務1運行:
set transaction isolation level read committed
begin tran
select * from customer        --看到3條記錄

現在切換到事務2,此時事務1還未結束。在事務2中運行:
set transaction isolation level read committed
begin tran
select * from customer
        --看到3條記錄,和事務1中相同
現在假設事務1事務繼續運行,修改數據並提交:
update customer set state = 'TK' where CustID = 3
commit

回到事務2,事務2根據先前查詢到的結果修改數據:
update customer set Zip = 99999 where state = 'NY'
commit

結果因爲事務1已經修改了事務2的where條件數據,事務2未成功修改數據(其實準確的說應該算是幻象讀引起的更新失敗。不過若滿足條件的記錄數多的話,事務2的update可能更新比預期的數量少的記錄數,也可算“丟失”了部分本應完成的更新。個人認爲只要明白實際上發生了什麼即可,不必過分追究字眼)。丟失更新還可能有別的情形,比如事務2也是
update customer set state = 'KO' where CustID = 3
兩個事務都結束後,事務2的結果反映到數據庫中,但事務1的更新丟失了,事務2也不知道自己覆蓋了事務1的更新。


3.髒讀演示
sqlserver的默認隔離級別是提交讀(read committed),當手工將其改爲未提交讀時,事務可以讀取其它事務沒提交的數據;oracle由於自身特殊實現機制,可以理解爲自身基礎的隔離級別就是可重複讀(與ANSI標準還是有區別的,後面例子會說明)。
SQL SERVER
打開事務1,運行:
begin tran
select * from customer
    update customer set state = 'TN' where CustID = 3

轉到事務2,運行:
set transaction isolation level read uncommitted
begin tran
select * from customer

此時看到的數據是事務1已經更新但還未提交的(3號記錄state值TN)。而如果事務1發覺數據處理有誤,轉到事務1,進行回滾:
    Rollback
此時事務2如根據剛讀取的數據進一步處理,會造成錯誤。它讀取的數據並未更新到數據庫,是“髒”的。

ORACLE
ANSI定義未提交讀(read uncommitted)級別本意不是爲了故意引入錯誤,而是提供一種可能的最大併發程度級別,即一個事務的數據更新不影響其它事務的讀取。Oracle從內核層面實現了更新數據不阻塞讀。可以說它提供未提交讀級別的兼容,但沒有髒讀問題。(詳情參考對應PPT文檔)故oracle沒有手工設置read uncommitted級別的語句。


4.不可重複讀

Sql server的默認級別沒有髒讀問題,但存在不可重複讀問題。Oracle默認級別也是提交讀,不過它因爲自身特殊機制,在語句一級不存在不可重複讀問題。也就是說當運行時間較長的查詢時,查詢結果是與查詢開始時刻一致的(即使查詢過程中其它事務修改了要查詢的數據),而SQL SERVER就存在問題(sql server 2005新特性提供了可選的語句一級一致性支持,叫做行版本機制,實際上可以說是照着oracle的多版本來的,大體原理差不多)。
由於語句一級的事務一致性難以演示,下面例子是事務一級,提交讀隔離級別下發生的不可重複讀現象:
SQL SERVER
打開事務1,運行:
set transaction isolation level read committed
begin tran
select * from customer where State = 'CA'

可以得到1條記錄,這個時候事務2中運行:
set transaction isolation level read committed
begin tran
update Customer set state = 'JP' where state = 'CA'
commit

事務2插入一條記錄並提交。回到事務1,事務1繼續運行,此時它再次相同的查詢,並藉此作進一步修改,卻發現讀取到的數據發生了變化。
select * from customer where State = 'CA'
--2次讀取不一致,之後的數據處理應該取消。否則不正確
update Customer set city = 'garden' where state = 'CA'
commit

讀取未能獲得記錄。也就是說在同一事務中兩次相同的查詢獲得了不同的結果,產生讀取不可重複現象。

ORACLE
儘管oracle在默認隔離級別下提供了語句級的事務讀一致性,但在事務級仍然是會出現不可重複讀現象。和sql server一樣,故不再重複。


5.幻像讀
當sqlserver的隔離級別設置爲可重複讀(repeatable read),可以解決上面例子出現的問題。其內部是通過事務期間保持讀鎖來實現的。
SQL SERVER
開始事務1,修改事務級別爲可重複讀,執行:
set transaction isolation level repeatable read
begin tran
select * from customer where State = 'CA'

和上例一樣得到1條記錄,這個時候事務2中運行:
set transaction isolation level repeatable read
begin tran
update Customer set state = 'JP' where state = 'CA'
commit

會發現事務2一直等待,並不結束。返回事務1,運行:
select * from customer where State = 'CA'        --2次讀取結果一致
update Customer set city = 'garden' where state = 'CA'
commit

事務2成功結束後,再返回事務1,發現事務1也完成了。通過鎖機制阻塞其它事務的修改,保持了事務期間讀取的一致性。然而,如果是插入數據,則還是會出現問題:
開始事務1,修改事務級別爲可重複讀,執行:
set transaction isolation level repeatable read
begin tran
select * from customer where State = 'CA'

得到1條記錄,這個時候事務2中運行:
set transaction isolation level repeatable read
begin tran
insert into customer values(4, 'hellow', 'world', 'paradise 001', 'garden', 'CA', 00000, 1119995555)
commit

發現事務2立刻提交併正常結束了。返回事務1,運行:
select * from customer where State = 'CA'
會發現得到了2條記錄。這種現象就叫做幻像讀。

ORACLE
由於自身特殊的機制,oracle沒有提供一致讀隔離級別的選項,想要獲得一致讀的效果,實際上需要將事務提升到串行化等級,即serializable。


6.串行化級別不同數據庫實現
在這個級別,可以認爲事務中的數據無論何時都是一致的,此級別使它顯得好像沒有其它用戶在修改數據,數據庫在事務開始時候被“凍結”(至少,對於本事務涉及的數據如此)。然而在不同數據庫中,其實現機制也不同。
SQL SERVER
開始事務1,運行:
set transaction isolation level serializable
begin tran
select * from customer where State = 'CA'

會得到1條記錄,這時事務2開始運行:
set transaction isolation level serializable
begin tran
insert into customer values(4, 'hellow', 'world', 'paradise 001', 'garden', 'CA', 00000, 1119995555)
commit

會發現事務2掛起,它在等待事務1結束。回到事務1,繼續:
select * from customer where State = 'CA'
update Customer set city = 'garden' where state = 'CA'
commit

在片刻的等待以後,事務1得到類似以以下格式消息:
消息1205,級別13,狀態56,第1 行
事務(進程ID 51)與另一個進程被死鎖在鎖資源上,並且已被選作死鎖犧牲品。請重新運行該事務。
而事務2更新了數據並正常結束。這是因爲兩個事務都設置成了串行化級別,當遇到衝突時候,sql server根據一定的規則選擇犧牲掉其中一個事務,來保證事務的串行性。上面的例子,如果將事務2的隔離級別改爲提交讀,那麼事務2會等待事務1完成,之後自己正常完成(因爲事務2沒有串行需求,不會有死鎖)。

ORACLE
在oracle中,通過多版本,可以在一定程度上避免死鎖。
開始事務1,運行:
set transaction isolation level serializable;
select * from customer where State = 'CA';    --set tran語句隱式開始事務

得到1條記錄,然後事務2開始運行:
set transaction isolation level serializable;
insert into customer values(4, 'hellow', 'world', 'paradise 001', 'garden', 'CA', 00000, 1119995555);
commit;

可以發現事務2立刻完成,沒有阻塞。回到事務1繼續:
select * from customer where State = 'CA';
update Customer set city = 'garden' where state = 'CA';
commit;

事務1中的第二次查詢和事務開始時刻一致,就好像事務2已經完成的提交不存在。事務最終正常更新完畢,並保持了“事務開始”時刻的數據一致性。
然而,如果事務1,2修改同樣的數據行,也會有錯誤,
開始事務1,運行:
set transaction isolation level serializable;
select * from customer where State = 'CA';    --set tran語句隱式開始事務

得到1條記錄,然後事務2開始運行:
set transaction isolation level serializable;
update customer set state = 'KO' where state = 'CA';
commit;

可以發現事務2立刻完成,沒有阻塞。回到事務1繼續:
select * from customer where State = 'CA';
update Customer set city = 'garden' where state = 'CA';
commit;

出現錯誤信息:
第 1 行出現錯誤:
ORA-08177: 無法連續訪問此事務處理
總的來說,oracle利用多版本的方式實現串行化級別更少造成死鎖,除非兩個事務修改了相同的數據行,一般也不會造成衝突。



7.不同隔離級別的相互影響
前面的例子基本都是兩個相同隔離級別事務的情況。如果不同隔離級別的事務發生衝突,會有什麼不同嗎?實際上,對於一個事務來說,其它事務的隔離級別對它來說是未知的,更進一步,甚至數據庫中有沒有其它事務,有多少事務也不知道。影響事務訪問數據就兩方面因素:該事務自身的隔離級別,該事務要訪問的數據上面鎖的狀態。
SQL SERVER
開始事務1,運行:
set transaction isolation level serializable
begin tran
select * from customer where State = 'CA'

事務1的查詢獲得1條記錄,轉到事務2,運行:
set transaction isolation level read uncommitted
begin tran
select * from customer

事務2查詢獲得3條記錄,回到事務1,運行:
update Customer set city = 'garden' where state = 'CA'
切換到事務2,運行:
select * from customer
update customer set state = 'KO' where state = 'CA'
commit;

因爲事務2隔離級別爲未提交讀,因此事務1中剛作的修改可以立刻從查詢看到,即使事務1還未結束。進一步的update因爲事務1對記錄加了獨佔鎖,因此事務2掛起。回到事務1將其提交:
Commit
事務1正常結束,獨佔鎖釋放,從而讓事務2得以繼續修改數據,並最終完成。

ORACLE
Oracle數據庫的隔離級別設置語句只有read committed和serializable(read only暫不討論),加上其特殊鎖機制,不同隔離級別事務間的影響除了上例(例6)中兩個都爲serializable的情況,其它都可視爲互不阻塞。

8.頁鎖與行鎖(限sql server)
Sql server的鎖可以到行一級。然而它又存在自動的鎖定擴大,鎖定轉換。因此存在一些意想不到的情況。下面是演示:
開始事務1,運行:
set transaction isolation level read committed
begin tran
select * from customer where State = 'CA'
update Customer set city = 'garden' where state = 'CA'

理論上來說,在提交讀級別下,上面的update語句只是在state值爲CA的數據行上加了獨佔鎖,表中其它數據應該可以被其它事務更新,然而,如下事務2開始:
set transaction isolation level read committed
begin tran
select * from customer
update customer set state = 'KO' where state = 'JP'
commit

發現事務2陷入阻塞狀態。儘管它們更新的不是同一條記錄。回到事務1,運行:
Commit
事務1結束後事務2才繼續運行至結束。
如果我們在表上加入索引,如下:
CREATE NONCLUSTERED INDEX [idx_state] ON [dbo].[Customer] (    [State])
再重複上面的步驟,會發現阻塞不再存在。
PS:這種現象應該和數據庫默認加鎖參數/機制有關,應該可以調整,但目前手中沒有進一步資料。故僅羅列了現象。

ORACLE
Oracle在數據一級只有一種數據行上的鎖,因此不存在sql server中這些問題。

9.Set transaction語句的作用週期
前面所有的例子,都是在會話窗口中進行的演示。一旦使用了set transaction語句,會影響整個會話。除非再顯式改變隔離級別,否則將保持到會話結束。例如:
開始事務1,假設會話一開始處於SQL SERVER的默認隔離級別(read committed):
begin tran
select * from customer where State = 'CA'
select * from sys.dm_tran_locks

系統視圖sys.dm_tran_locks可以查看當前的加鎖情況,到目前位置,只有數據庫級的鎖。繼續運行:
set transaction isolation level repeatable read
select * from customer where State = 'CA'
select * from sys.dm_tran_locks
commit

接下來的語句改變了隔離級別到可重複讀,接下來的查詢,會看到行級鎖的記錄。在上面事務提交後,運行:
begin tran
select * from customer where State = 'CA'
select * from sys.dm_tran_locks
commit

仍然會從視圖sys.dm_tran_locks看到行級鎖記錄。整個會話期間都受到影響。

但是,如果調用存儲過程,函數,則過程/函數中事務隔離級別的改變並不會對調用環境造成影響。可以通過以下例子說明,首先,創建一個存儲過程腳本:
CREATE PROCEDURE [dbo].[test_tran_level]
AS
BEGIN
    BEGIN TRAN
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
    SELECT * FROM CUSTOMER
    UPDATE CUSTOMER SET STATE = 'SS' WHERE CustID = 3
    SELECT * FROM sys.dm_tran_locks
    COMMIT
END

然後,在會話窗口調用該過程,會話窗口當前隔離級別爲默認的提交讀:
Exec test_tran_level
運行的結果可以看到讀取鎖信息,再在會話中運行:
begin tran
select * from customer where State = 'CA'
select * from sys.dm_tran_locks
commit

視圖sys.dm_tran_locks並未有讀鎖的記錄,說明事務隔離級別仍然是提交讀。

發佈了38 篇原創文章 · 獲贊 6 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章