1. 程式人生 > >postgres -- 一個問題引發的事務探究(二)

postgres -- 一個問題引發的事務探究(二)

這一篇來了解下 資料庫 的隔離機制

問題研究只針對Postgres 資料庫,不針對mysql或者其他。

說道隔離機制,就要先說下資料庫事務的4大特性:

⑴ 原子性(Atomicity)
  原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,這和前面兩篇部落格介紹事務的功能是一樣的概念,因此事務的操作如果成功就必須要完全應用到資料庫,如果操作失敗則不能對資料庫有任何影響。
⑵ 一致性(Consistency)
  一致性是指事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。
  拿轉賬來說,假設使用者A和使用者B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個使用者的錢相加起來應該還得是5000,這就是事務的一致性。
⑶ 隔離性(Isolation)
  隔離性是當多個使用者併發訪問資料庫時,比如操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
  即要達到這麼一種效果:對於任意兩個併發的事務T1和T2,在事務T1看來,T2要麼在T1開始之前就已經結束,要麼在T1結束之後才開始,這樣每個事務都感覺不到有其他事務在併發地執行。
⑷ 永續性(Durability)
  永續性是指一個事務一旦被提交了,那麼對資料庫中的資料的改變就是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作。
  例如我們在使用JDBC操作資料庫時,在提交事務方法後,提示使用者事務操作完成,當我們程式執行完成直到看到提示後,就可以認定事務以及正確提交,即使這時候資料庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是資料庫因為故障而沒有執行事務的重大錯誤。

這裡要說下重複讀和幻讀的點:重複讀是指不能重複讀當前的記錄,因為它會變。幻讀只的是數目,會出現原本查詢範圍之外的資料。

針對 文章(一)中出現的問題,就是就是隔離性 造成的,所以我們去研究一下 資料庫的隔離機制:

隔離機制髒讀不可重複讀幻讀
Read uncommitted允許,但不是在PG中可能可能
Read committed不可能可能可能
Repeatable read不可能不可能允許,但不是在PG中
Serializable不可能不可能不可能

在PostgreSQL中,你可以請求四種標準事務隔離級別中的任意一種。 但是在內部,只實現了三種不同的隔離級別,即:PostgreSQL的讀未提交模式的行為類似於讀已提交。 這是因為這是把標準的隔離級別對映到PostgreSQL的多版本併發控制架構的唯一合理方法。該表還顯示了PostgreSQL的重複讀不允許幻讀。SQL 標準允許更嚴格的行為: 四種隔離級別只定義了哪種現像不能發生,沒有定義哪種現像一定

發生。 可用的隔離級別的行為在下面小節中詳細描述。 (本段出自Postgres官方手冊中)

檢視當前PG使用的隔離級別 :

select current_setting('transaction_isolation');
"read committed"

本地資料庫沒有設定過,預設級別 好像就是 讀已提交。

1.Read uncommitted(讀未提交)

一個事務在執行過程中可以看到其他事務沒有提交的新插入的記錄,而且能看到其他事務沒有提交的對已有記錄的更新。

髒讀(Dirty reads)就會可能發生於此隔離機制下,一個事務讀取了另一個事務改寫但尚未提交的資料時。如果改寫在稍後被回滾了,那麼第一個事務獲取的資料就是無效的。

但是由上面表格其實可以知道,在PG中是不會出現髒讀的,即使你設定成讀未提交也沒法測試,路過而已。

2.Read committed(讀已提交)

這是PG的預設隔離級別。

當一個事務執行使用這個隔離級別時, 一個查詢(沒有FOR UPDATE/SHARE子句)只能看到查詢開始之前(1)已經被提交的資料, 而無法看到未提交的資料或在查詢執行期間其它事務提交的資料(2)。實際上,SELECT查詢看到的是一個在查詢開始執行的瞬間該資料庫的一個快照。不過SELECT可以看見在它自身事務中之前執行的更新的效果(3),即使它們還沒有被提交。還要注意的是,即使在同一個事務裡兩個相鄰的SELECT命令可能看到不同的資料(4), 因為其它事務可能會在第一個SELECT開始和第二個SELECT開始之間提交。(此為文件原話)。

此段是什麼意思,有幾處標紅的,需要特別注意一下。

1. 表明當前select查詢的資料是從 當前 查詢開始的時候,注意並不是select所處的事務開始的時候。這邊容易出現誤解。

2. 表明一旦執行了當前查詢,那麼在此之後提交的資料不能被讀取到。

3.表明select可以讀取到自身事務中之前的更新資料。

4.表明select可能會讀取到不同的資料,當select2和select1期間其他事務做了提交,更新了資料。這就是所謂的不可重複讀。

測試一下讀已提交 隔離機制:

表結構:

CREATE TABLE public.student
(
  id smallint,
  name character(50),
  phone character(50)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE public.student
  OWNER TO postgres;

插入資料:

insert into student(id,name,phone) values (1,'張三','111222');
insert into student(id,name,phone) values (2,'李四','111222');
insert into student(id,name,phone) values (3,'王五','111222333');
insert into student(id,name,phone) values (4,'麻子','111222444');
當前資料顯示為4條:
1;"張三                                                ";"111222                                            "
2;"李四                                                ";"111222                                            "
3;"王五                                                ";"111222333                                         "
4;"麻子                                                ";"111222444                                         "
測試用例 1
事務A:
start transaction;
--1 第一次查詢,只有4條記錄。
select * from student;
--2 第二次查詢之前,事務2只是insert了資料,沒提交,查詢還是隻有4條
select * from student;
--3 第三次查詢之前,事務2提交了資料,查詢資料有5條。
select * from student;

事務B:
start transaction;
insert into student(id,name,phone) values (5,'麻子兒子','111222444');
commit;

從資料查詢來看,只能看到查詢開始時的一個數據庫快照,或者能看到其他事務已經提交的更新,未提交的更新看不到。

有個說明需要注意下,就是select語句中帶有了 for update 語句,順帶測試一下。

測試用例 2

事務A:

start transaction;
select * from student for update;

事務B:
start transaction;
update student set phone = '66666' where id = 1;

1.當執行事務A時,發現可以讀出當前資料。但是此時如果執行事務B,就會發現沒有資訊通知,此處表明了事務B被阻塞了,也就是在 for update期間,是不允許其他事務對滿足條件了資料進行任何修改,除非事務A的事務被釋放了,此時發現事務B出現了提交按鈕,顯示此時可以提交,提交事務B,資料被正常修改。

事務B被重新執行時的時間,發現是01.54之後了。

-- 執行查詢:
start transaction;
update student set phone = '66666' where id = 1;
Query returned successfully: one row affected, 01:54 minutes execution time.
測試用例 3

2.1 那麼我們試一試,先執行事務B呢?發現事務B可以正常執行,如下圖:

-- 執行查詢:
start transaction;
update student set phone = '77777' where id = 1;
Query returned successfully: one row affected, 11 msec execution time.
2.2 但是如果此時執行事務A,發現,沒有回覆,也即是說明事務A此時被阻塞了,
-- 執行查詢:
start transaction;
select * from student for update;
2.2 此時commit 事務B:發現事務A被執行了:資料也是事務B執行更新的資料。
-- 執行查詢:
COMMIT;
Query returned successfully with no result in 21 msec.
-- 執行查詢:
start transaction;
select * from student for update;

Total query runtime: 01:12 minutes
檢索到 5 行。

那麼可以發現在使用for update的情況下,如果已經開啟了查詢的事務的話,那麼在事務沒有被釋放期間,另外的事務是不能修改資料的。

如果已經在修改資料的事務期間,for update查詢的事務也是不能執行的,需要等到修改事務提交之後才能查詢出資料。

另外說明下,不可重複讀和幻讀的強調點不同,不可重複讀強調了讀取之前讀取過的資料,發現已經被其他事務更新過。

幻讀強調重新執行執行一個之前的查詢,發現數據發生了改變。其他點看起來是一樣的。都是資料改變了。

copy一下文件中的描述:

UPDATEDELETESELECT FOR UPDATESELECT FOR SHARE命令在搜尋目標行時的行為和SELECT一樣: 它們將只找到在命令開始時已經被提交的行。(1) 不過,在被找到時,這樣的目標行可能已經被其它併發事務更新(或刪除或鎖住)。在這種情況下, 即將進行的更新將等待第一個更新事務提交或者回滾(如果它還在進行中)。 如果第一個更新事務回滾,那麼它的作用將被忽略並且第二個事務可以繼續更新最初發現的行。 如果第一個更新事務提交,若該行被第一個更新者刪除(2),則第二個更新事務將忽略該行,否則第二個更新者將試圖在該行的已被更新的版本上應用它的操作(2)。該命令的搜尋條件(WHERE子句)將被重新計算來看該行被更新的版本是否仍然符合搜尋條件。如果符合,則第二個更新者使用該行的已更新版本繼續其操作。在SELECT FOR UPDATESELECT FOR SHARE的情況下,這意味著把該行的已更新版本鎖住並返回給客戶端。  該命令的搜尋條件(WHERE子句)將被重新計算來看該行被更新的版本是否仍然符合搜尋條件。(3)如果符合,則第二個更新者使用該行的已更新版本繼續其操作。在SELECT FOR UPDATESELECT FOR SHARE的情況下,這意味著把該行的已更新版本鎖住並返回給客戶端。      

1. 注意update和delete操作都是和select一致,查詢出的都是命令開始時已經被提交的行。

2.那麼如果該行被刪除的話,則第二個操作如果是更新操作的話會忽略該行(此處應該是update或者delete都會被hulv),否則則是在該行已被更新的版本上應用它的操作。

3.注意此處也表明where 條件是需要重新判斷的,注意點。

測試用例 4:

測試用例4 可以引用第一篇文章的問題 測試例項,不在描述。

測試用例 5:

基礎資料如下:

2;"李四                                                ";"111222"

事務A:

start transaction;
delete from student where id = 2;
insert into student(id,name,phone) values (2,'李四','aaa111222');
事務B:
start transaction;
update student set phone = '33333333' where id= 2;

執行事務A時,可見如下:刪除成功了,並且新增了一條資料,phone為aaa111222
-- 執行查詢:
start transaction;
delete from student where id = 2;
insert into student(id,name,phone) values (2,'李四','aaa111222');
Query returned successfully: one row affected, 12 msec execution time.
此時執行事務B被阻塞了,直到事務A被提交:但是執行語句中確發現,0行受到影響,可見,在事務A刪除的那條語句中,事務B的更新操作直接被忽略了,但是綜合測試用例4來看,新增操作不會被忽略,而是基於事務A的資料,新增另外一條。
-- 執行查詢:
start transaction;
update student set phone = '33333333' where id= 2;
Query returned successfully: 0 rows affected, 10.5 secs execution time.
此時的資料如下:僅僅是事務A的新增操作出來的資料。
2;"李四                                                ";"aaa111222                                         "

3.Repeatable read(可重複讀)

以下copy文件描述,基於此分析:

可重複讀隔離級別只看到在事務開始之前被提交的資料(1);它從來看不到未提交的資料或者並行事務在本事務執行期間提交的修改(2)(不過,查詢能夠看見在它的事務中之前執行的更新,即使它們還沒有被提交),這是標準特別允許的,標準只描述了每種隔離級別必須提供的最小保護。

這個級別與讀已提交不同之處在於,一個可重複讀事務中的查詢看到 事務中第一個非事務控制語句開始時的一個快照, 而不是事務中當前語句開始時的快照。(3) 因此,在一個單一事務中的後續SELECT 命令看到的是相同的資料,即它們看不到其他事務在本事務啟動後提交的修改。    


基於1,2,3可以發現,可重複讀機制 讀取的資料是根據事務開始的時間,而不是 當前語句的時間,注意一下。那麼也就是在這個事務內,其他事務的修改對它都是不可見的,無論執行多少次select,查詢的資料都是此次事務開啟時讀取的。

測試用例 6:

事務A:

start transaction ISOLATION LEVEL Repeatable read;
delete from student where id = 4;
事務B:
start transaction ISOLATION LEVEL Repeatable read;
update student set phone = 'bbbbbbbbb22222' where id = 4;
執行事務A時如下:
-- 執行查詢:
start transaction ISOLATION LEVEL Repeatable read;
delete from student where id = 4;
Query returned successfully: one row affected, 10 msec execution time.
此時執行事務B:
ERROR:  could not serialize access due to concurrent update
********** 錯誤 **********

ERROR: could not serialize access due to concurrent update
SQL 狀態: 40001
提交事務A:事務B也能正常提交,只不過在執行期間出錯。

測試用例 7:

源資料:

6;"麻子兒子                                              ";"ooooooo                                           "

事務A:

start transaction ISOLATION LEVEL Repeatable read;

select * from student;

事務B:
start transaction ISOLATION LEVEL Repeatable read;

update student set phone = '1111' where id = 6;
執行事務A:但是不啟動select語句:此時執行事務B,
-- 執行查詢:
start transaction ISOLATION LEVEL Repeatable read;

update student set phone = '111' where id = 6;
Query returned successfully: one row affected, 10 msec execution time.
此時執行事務A中的select語句,發現 並沒有改成 111,而還是ooooooo,因此在事務A的事務開啟之後,只能讀取到當前事務開啟時的快照,後續事務的修改對其不可見。

但是 如果使用這個隔離機制就需要 使用這個級別的應用必須準備好由於序列化失敗而重試事務。因為一個可重複讀事務無法修改或者鎖住被其他在可重複讀事務開始之後的事務改變的行。當一個應用接收到這個錯誤訊息,它應該中斷當前事務並且從開頭重試整個事務。在第二次執行中,該事務將見到作為其初始資料庫檢視一部分的之前提交的改變,這樣在使用行的新版本作為新事務更新的起點時就不會有邏輯衝突(文件原話,注意點)

此隔離機制一般不常用,重試事務這個方法也不是這篇部落格關注點,暫時記錄。

4.Serializable(序列化)

源文件如下copy:

可序列化隔離級別提供了最嚴格的事務隔離。這個級別為所有已提交事務模擬序列事務執行;就好像事務被按照序列一個接著另一個被執行,而不是並行地被執行。但是,和可重複讀級別相似,使用這個級別的應用必須準備好因為序列化失敗而重試事務。事實上,這個給力級別完全像可重複讀一樣地工作,除了它會監視一些條件,這些條件可能導致一個可序列化事務的併發集合的執行產生的行為與這些事務所有可能的序列化(一次一個)執行不一致。這種監控不會引入超出可重複讀之外的阻塞,但是監控會產生一些負荷,並且對那些可能導致序列化異常的條件的檢測將觸發一次序列化失敗。    

序列化 隔離機制 內同一事務的 讀和 可重複讀是一致的,只能讀取從事務開始時的快照,所以不可能出現 重複讀的問題。

但是幻讀在 可重複讀是 沒有解決的,可重複讀解決的問題主要可以看成是對 同一條資料保證了一致性,但是沒有保證對突然出現的行資料的一致性。

序列化解決的就是這個問題。

測試用例 8:

源資料有4條:

事務A:

start transaction ISOLATION LEVEL Serializable;
select * from student;
事務B:
start transaction ISOLATION LEVEL Serializable;
insert into student(id,name,phone) values (7,'麻子孫子','111222444');
執行發現當執行事務A時,可以讀取4條資料,接著執行事務B,發現成功插入一條資料,但是此時再次執行事務A的查詢,發現還是4條,沒有出現事務B新增的那一條,解決了幻讀的情況。

參考文獻:http://www.postgres.cn/docs/9.5/index.html