1. 程式人生 > >資料庫事務隔離級別例項探討

資料庫事務隔離級別例項探討

我們知道,資料一般有如下四種隔離級別
  • 0.  read uncommitted (讀未提交)
  • 1.  read committed (讀已提交)
  • 2.  repeatabale read (可重複讀)
  • 3.  serializable read (序列化讀)

下面通過例項介紹這四種隔離級別。

準備工作

首先,準備工作, 我使用的資料庫是Sybase, 我們在資料庫裡建一個測試表,並插入資料:

create table test_table (
  col1 int,
  col2 varchar(15)
)
go
alter table test_table lock datarows 
go
create unique index ux_idx1 on test_table (col1)
go

insert test_table (col1, col2) values (1, "test")
insert test_table (col1, col2) values (2, "test")
insert test_table (col1, col2) values (3, "test")
insert test_table (col1, col2) values (4, "test")
insert test_table (col1, col2) values (5, "test")
go

顯示一下資料:
col1        col2           
 ----------- ---------------
           1 test           
           2 test           
           3 test           
           4 test           
           5 test           

(5 rows affected)

注意上面建表的時候,把表的鎖改為了行模式

alter table test_table lock datarows 
go

現在開啟兩個視窗, 分別叫做視窗A,和視窗B。

讀未提交

設定視窗A的事務隔離級別為 0 (讀未提交)

SET TRANSACTION ISOLATION LEVEL 0
go

在視窗B中,執行如下SQL
begin tran

update test_table 
 set col2 = "TEST UPDATE"
where col1 = 1
go

我們看到輸出:

(1 row affected)
表示上面的SQL執行成功。 注意: 上面的SQL開啟了一個事務,但是事務並沒有提交。

這時,在視窗A,執行如下SQL,檢視同一條記錄(col1=1)的值:

select * from test_table where col1 = 1
go


我們看到輸出:
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    

(1 row affected)

我們看到視窗B裡更新後的值。也就是,B窗口裡並沒有提交的資料, 我們在視窗A中讀到了。這就是讀未提交。

讀已提交

下面介紹讀已提交,這時候,把視窗A的事務隔離級別設定為1(讀已提交)

SET TRANSACTION ISOLATION LEVEL 1
go

並在視窗A執行如下SQL

select * from test_table where col1 = 1
go


這時候,我們發現上面的SQL語句阻塞了。因為事務隔離級別是讀已提交,視窗A中的事務和視窗B中的事務操作了同一條記錄, 它在等待視窗B中的事務提交或者回滾。如果視窗B中的事務沒有完成,它就一直等待下去。

這時候,我們提交視窗B中的事務,視窗B執行:

commit
go

現在,再看視窗A, 剛才的阻塞解除了,視窗A看到如下輸出:
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    

(1 row affected)

視窗A的事務,讀到了視窗B提交後的資料。

總結:事務隔離界別設定為讀已提交,只有提交後的資料才能被讀取。如果當前事務讀取的記錄在另外一個事務中更新了,且還沒有的提交,當前讀取操作會被阻塞。

可重複讀

再來介紹可重複讀,可重複讀對應的就是不可重複讀,先介紹什麼是不可重複讀。

現在視窗A中的事務隔離級別還是讀已提交,先不用改它。我們在視窗A中執行如下SQL

begin tran 
select * from test_table where col1 = 2
go

我們立即看到輸出:
 col1        col2           
 ----------- ---------------
           2 test           

(1 row affected)
上面的SQL,開啟了一個事務,並讀了一行資料 col2=2, 但是並沒有提交事務。

這時,在視窗B中,我們執行如下SQL,修改col2=2的值:

update test_table 
 set col2 = "TEST UPDATE"
where col1 = 2
go
SQL執行成功,立即看到輸出:
(1 row affected)
表示更新成功。

這時候,回到視窗A,在來查詢 col2=2的值,

select * from test_table where col1 = 2
go
col1        col2           
----------- ---------------
          2 TEST UPDATE    

(1 row affected)

這時,讀出來的值是視窗B中更新後的值。這樣問題就來了,在視窗A,同一個事務裡,兩次讀取同一個值,返回的結果不一樣,這就是不可重複讀。

視窗A兩次讀取的完整SQL和輸出如下:

begin tran 
select * from test_table where col1 = 2
go
col1        col2           
----------- ---------------
          2 test           

(1 row affected)
 
select * from test_table where col1 = 2
go
col1        col2           
----------- ---------------
          2 TEST UPDATE    

(1 row affected)

現在看看,什麼是可重複度。可重複度,顧名思義就是,在同一個事務裡,多次讀取的同一個值,前後應該一致。

我們提交視窗A中的事務

commit
go

我們以col1=3講解可重複度。

在視窗A中設定事務隔離界別為可重複讀:

SET TRANSACTION ISOLATION LEVEL 2
go
在視窗A執行如下SQL:
begin tran 
select * from test_table where col1 = 3
go

看到輸出:
 col1        col2           
 ----------- ---------------
           3 test           

(1 row affected)

切換到視窗B中,執行如下SQL,更新同一條記錄(col1=3)
update test_table 
 set col2 = "TEST UPDATE"
where col1 = 3
go
 

這時候發現上面的SQL語句阻塞了,上面的更新等待視窗A的事務完成。

在視窗A中,再次執行如下SQL

select * from test_table where col1 = 3
go
輸出如下:
 col1        col2           
 ----------- ---------------
           3 test           

(1 row affected)

和上一次查詢結果相同。視窗A中的同一個事務內,2次或者多次對同一個資源(這裡是col1=3)讀取的結果是一致的,這就是可重複讀。

我們在視窗B,按Ctrl + C,把視窗B中SQL break掉。我們看看,在視窗B,能不能update 另外的一條記錄 col1=4, 視窗B中執行:

update test_table 
 set col2 = "TEST UPDATE"
where col1 = 4
go

 SQL執行成功,立即返回了:
(1 row affected)

這時,我們在視窗A中,檢視col1=4的記錄:
select * from test_table where col1 = 4
go

馬上就得到了輸出:
 col1        col2           
 ----------- ---------------
           4 TEST UPDATE 

這時候再回到視窗B,再次 update  col1=4的記錄,

update test_table 
 set col2 = "TEST UPDATE UPDATE"
where col1 = 4
go
 

這時發現,上面的update阻塞了。

回到視窗A,查詢col1=4的記錄:

select * from test_table where col1 = 4
go
 col1        col2           
 ----------- ---------------
           4 TEST UPDATE    

(1 row affected)

值還是原來的 “TEST UPDATE”。

這時,我們提交A視窗的事務,在視窗A執行:

commit
go


這時,視窗B中被阻塞的更新操作立即得到了執行,視窗B中輸出:

(1 row affected)

查詢一下:
select * from test_table where col1 = 4
go
 col1        col2           
 ----------- ---------------
           4 TEST UPDATE UPD

(1 row affected)


總結:如果A事務設定為可重複讀, 當它讀取了資源R,但還沒有提交事務時,其他事務就不能更新資源R,對它沒有讀取的資源,其它事務是可以更新的。

上面的例子是以單條記錄作為資源的, 如果使用select * 呢?

如果在視窗A中, 使用如下SQL

SET TRANSACTION ISOLATION LEVEL 2
go
begin tran
select * from test_table 
go

輸出:

 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           


上面,select * 讀取整張表,這時候,在視窗B中對 test_table 表中的已有記錄更新操作都會被阻塞:

視窗B執行:

 update test_table 
  set col2 = "TEST UPDATE"
 where col1 = 5
 go
上面的SQL會被阻塞 (Ctrl +C 中斷阻塞)。

視窗B執行:

 delete test_table where col1 = 1
 go

上面的SQL同樣會被阻塞(Ctrl +C 中斷阻塞)。

上例說明,可重複讀對 select *  已有資料是適用的。

那麼對新插入的資料呢?看下面的例子:

我們新插入一條記錄,視窗B執行:

insert test_table (col1, col2) values (6, "test")
go

輸出:
(1 row affected)

說明插入操作執行成功。

回到視窗A, 執行

select * from test_table 
go

輸出:
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           
           6 test           

(6 rows affected)

這時,視窗A中事務,讀到了視窗B中新插入的資料。

視窗A中,完整的SQL和輸出如下:

SET TRANSACTION ISOLATION LEVEL 2
go
begin tran
select * from test_table 
go
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           

(5 rows affected)
select * from test_table 
go
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           
           6 test           

(6 rows affected)


從上面的輸出可以看出,雖然事務隔離級別設定可重複讀,但是,同一個事務中,上面兩次select * 操作,返回的結果是不同的。這個問題就是範讀。

要解決泛讀的問題,就需要提高事務的隔離界別到序列化讀。

序列化讀

最後來看看序列化讀,提交視窗A中的事務,並設定事務隔離級別到序列化讀,視窗A中執行:

commit tran
go

SET TRANSACTION ISOLATION LEVEL 3
go
現在視窗A執行如下SQL:
begin tran
select * from test_table 
go
輸出:
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           
           6 test           

(6 rows affected)


切換到視窗B中,執行:
insert test_table (col1, col2) values (7, "test")
go

這是,視窗B中,上面的SQL語句被阻塞了,插入操作不能完成。

視窗A中,執行select *:

select * from test_table 
go

輸入和上次的一樣:

 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           
           6 test           

(6 rows affected)
可見,序列化解決了泛讀的問題。



視窗A中,執行

 commit tran
 go

這時,視窗B阻塞解除,
(1 row affected)
查詢一下:
select * from test_table
go
 col1        col2           
 ----------- ---------------
           1 TEST UPDATE    
           2 TEST UPDATE    
           3 test           
           4 TEST UPDATE UPD
           5 test           
           6 test           
           7 test           

(7 rows affected)