資料庫事務隔離級別例項探討
- 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)