80年代中國人結婚四大件:手表、自行車、縫紉機、收音機(三轉一響)。要把事務娶回家需要四大件,所以事務很刻薄(ACID),四大件清單:原子性(Atom)、一致性(Consistent)、隔離性(Isolate)、持久性(Durable)。ACID就是數據庫事務正確執行的四個基本要素的縮寫。
- 原子性:要么不談,要談就要結婚!
對於其數據修改,要么全都執行,要么全都不執行。如果系統只執行這些操作的一個子集,則可能會破壞事務的總體目標。最典型的問題就是銀行轉帳問題。
- 一致性:戀愛時,什么方式愛我;結婚后還得什么方式愛我;
數據庫原來有什么樣的約束,事務執行之后還需要存在這樣的約束,所有規則都必須應用於事務的修改,以保持所有數據的完整性。事務結束時,所有的內部數據結構(如 B 樹索引或雙向鏈表)、完整性約束(索引、主鍵)都必須是一致的。
- 隔離性:鬧完洞房后,是倆人的私事。
事務查看數據時數據所處的狀態,要么是另一並發事務修改它之前的狀態,要么是另一事務修改它之后的狀態,事務不會查看中間狀態的數據。當事務可序列化時將獲得最高的隔離級別。隔離性是事務機制里相對來說,比較復雜的,下文另說。
- 持久性:一旦領了結婚證,無法后悔。
修改即使出現致命的系統故障也將一直保持。不要告訴我系統說commit成功了,回頭電話告訴我,服務器機房斷電了,我的事務涉及到的數據修改可能沒有進入數據庫。
拋開剛才的四大件比方,再談隔離性。隔離性是指DBMS可以在並發執行的事務間提供不同級別的數據讀寫安全隔離措施。隔離的級別和並發事務的吞吐量之間存在反比關系。較高級別的隔離可能會帶來較高的沖突和較多的事務流產。流產的事務要消耗資源,這些資源必須要重新被訪問。所以這需要trade-off,到底是訪問速度優先,還是數據絕對正確優先。
兩台機器分別向數據庫插入數據,在提交事務前再查詢一次數據庫,此時本機器的讀操作可以讀到本機器尚未提交的事務的數據嗎?這個問題本身就是一種意識錯誤,事務是一個原子單位,任何隔離級別都改變不了這個真理。
我們先了解幾個數據庫不得不知的秘密,事務一旦啟動,寫寫肯定是沖突的。那么讀寫呢?讀分成兩種,被別人讀和讀別人的數據。被別人讀會產生臟數據的問題。讀別人的會產生不可重復讀和幻讀兩種情況。
- 臟讀:讀到的數據不是此刻真實的數據。
臟讀就是指當一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然后使用了這個數據。打個比方:
我的支付寶余額3000元,事務A改為2000,但事務A尚未提交。與此同時,事務B正在讀取,並且“成功”讀取到余額為2000。隨后,事務A由於網絡或者IO原因執行失敗,回滾成3000元,事務B拿到的值繼續運算,就是錯誤的,這算是一次dirty read。 ' T, B
銀行轉帳,A給B轉1000元,B+1000,這個時候,B就能夠讀取到。這個時候A還沒有減掉1000元,后悔了,沒有提交,這個時候B把錢提走了,這不扯嗎? 銀行在早年代出現類似情形的bug.
- 不可重復讀:讀了兩次,值不一樣。
一個事務對同一行數據重復讀取兩次,但是卻得到了不同的結果。例如,在兩次讀取的中途,有人對該行數據進行了update操作,並提交,結果就讓當然這個事務郁悶了…
還是余額3000元,事務A是一個比較長的事務,一開始讀取到3000,結果恰好我的水電自動扣款100成功(事務B執行成功),事務A在最后讀取到的余額成了2900元。這就是不可重復讀現象。
- 幻讀:原來沒有,現在有了…
事務在操作過程中進行兩次查詢,第二次查詢的結果包含了第一次查詢中未出現的數據(這里並不要求兩次查詢的SQL語句相同)。這是因為在兩次查詢過程中有另外一個事務插入數據造成的。
事務A正在統計到目前為止的訂單數量,一開始讀到的是10筆。結果恰好這個時候,家人用此支付寶買了一個家電。等事務A打算提交的時候發現成了11筆。
剛才說到了這三種應用事務產生的讀寫問題,事務產生了對應的4種隔離級別,在Mysql中利用如下:
SELECT @@TX_ISOLATION; 即可看到:說明Mysql的默認是可重復讀取。 select @@global.tx_isolation; 查看全局的事務隔離級別。現在我們按照java.sql.Connection定義的四個常量值說開來:
- 讀未提交:TRANSACTION_READ_UNCOMMITTED = 1;
允許臟讀取,但不允許更新丟失。如果一個事務已經開始寫數據,則另外一個數據則不允許同時進行寫操作,但允許其他事務讀此行修改后卻沒有提交的數據,就是擔心人家的事務出問題回滾(ROLLBACK)了,而你還拿這個臟數據繼續計算。該隔離級別可以通過“排他寫鎖”實現。絕大部分的數據庫沒有二到這個地步。
設置:SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
表現:可以讀取任何數據,但是如果更新到同一數據上,需要等待另一個事務執行完,有超時異常:
注意,即使是GLOBAL設置,也需要打開一個新的會話連接,才能生效,包括當前設置連接。如果是SESSION,當前會話馬上生效,但絕不會影響到其它會話的隔離級別,使用SELECT @@TX_ISOLATION; 檢查一下當前的隔離級別,免得穿越到秦國。任何Mysql設置,首先要清楚它是全局的,還是會話級別的。
- 讀提交:TRANSACTION_READ_COMMITTED = 2;
允許不可重復讀取,但不允許臟讀取。這可以通過“瞬間共享讀鎖”和“排他寫鎖”實現。讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。
設置:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
表現:對同一數據更新需要等待,一個事務如果沒有COMMIT,任何其它事務無法讀取它的中間值。因為只是加了行共享鎖,所以此時,還是可以讀到一個事務里正在被update的數據。這里的問題是一個事務你還可以讀另外一個事務正在更新的數據。
- 可重復讀取:TRANSACTION_REPEATABLE_READ = 4;
禁止不可重復讀取和臟讀取,但是有時可能出現幻影數據,但在innodb中此隔離級別不允許幻象讀,應該說這已經是較高級別的安全保證了。這可以通過“共享讀鎖”和“排他寫鎖”實現。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。
設置:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
表現:除了之上兩個要求之后,如果一個事務對一行的讀取,即使其它事務的的確確已經修改了此項數據,他也還是會將錯就錯到底,不會去讀這條新值,保證一個事務開始后,讀取的任何數據都一份
4. 序列化:TRANSACTION_SERIALIZABLE = 8;
與“可重復讀取”隔離最大的區別是 讀也會加鎖,另外事務無法更新。它要求事務序列化執行,事務只能一個接着一個地執行,但不能並發執行。如果僅僅通過“行級鎖”是無法實現事務序列化的,必 須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。任何數據的插入與更新,都是慢慢來,象單跑道起飛的飛機一樣。在序列化隔離中,innodb會對每一個select語句后自動加上lock in share mode.
設置:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
表現:它依然可以同時起多個事務,但是如果對同一數據進行的任何讀寫操作,都必須一個等待另一個執行完再說,原則是先到的有鎖定權。如果你執行一個update,對方也來一個update,那么出現:
在Java中,修改事務隔離級別,void setTransactionIsolation(int level) throws SQLException;
如果出現兩邊會話設置隔離級別不一致的情況,屬性互相獨立,以更高隔離級別為准。實際上隔離級別只針對於鎖。設定對應的隔離級別,對應的操作都有對應的鎖去執行現場清理工作。鎖事實上只有兩種:
A. 共享鎖(Shared Lock) 也叫讀鎖.
共享鎖表示對數據進行讀操作。因此多個事務可以同時為一個對象加共享鎖。
B.寫鎖(Write Lock)也叫排它鎖
寫鎖表示對數據進行寫操作。如果一個事務對對象加了排他鎖,其他事務就不能再給它加任何鎖了。
鎖粒度 |
本連接查詢 |
本連接更新 |
其它連接查詢 |
其它連接insert |
其它連接update |
LOCK TABLES 表名 READ; |
可以 |
不可以 |
可以 |
不可以 |
不可以 |
LOCK TABLES 表名 WRITE; |
可以 |
可以 |
不可以 |
不可以 |
不可以 |
隔離級別的任何實現都離不開鎖,DBMS,有一個善良而忠實的看門老者,不停地在給你開門、鎖門、防止沖突產生。要mysql鎖表的操作是LOCK,而解鎖的操作是UNLOCK.
LOCK TABLES 表名 READ LOCAL; 其它連接的insert/update都無法做。(innodb)
在innodb中兩個事務之間對於不同行的操作是可以的,所以它們實現的是行鎖。
開始事務時,必須關閉自動提交,SET AUTOCOMMIT=0; 那么在AUTOCOMMIT=0之前的語句會順利執行並逐條提交。支持事務的表必須是engine = INNODB。當錯誤時,全盤檢查所有ROLLBACK到start位置。ROLLBACK相當於一個標記點,凡是打此標記的地方,都會被自動回滾。如果沒有ROLLBACK,那么即使是autocommit=0,即會被正常提交,無法實現數據的回滾。
COMMIT對於單個事務事實上可有可無。因為end$$會強制提交事務。但在多個事務處理時,必須在某個點提交,否則回滾時存在問題。
只是如果AUTOCOMMIT默認為1,即使有SQL語句包含在事務里邊,也是每條自動提交。注意這個時間的提交只是當前連接的操作員自己在YY,因為沒有COMMIT,無論AUTOCOMMIT的值等於多少,都會不提交到數據庫,讓別的人看到。但這個時候非常有意思,因為自動提交為1,如果此事務包括的存儲過程被再次調用一次,由於又開始一個新事務,會自動提交上一次的那個沒有COMMIT的事務。所以就危害性來說,SET AUTOCOMMIT=0危害性要大得多。
即使COMMIT封閉了START TRANSACTION也無法自動把 AUTOMMIT自動置為1,這就是解釋陳良允同學為什么那次操作,我無法看到結果,因為它的連接已經執行,卻沒有自動提交。可以在他的連接上看到,因為對他來說只是一個臨時操作。
事務的開始、回滾、提交與否,與AUTOCOMMIT=0的值的改變沒有任何關系!所以高度注意:前提是置AUTOCOMMIT=0,這時如果沒有封閉一個事務是極其危險的,因為它非常容易引起鎖表,這樣影響就是全局性的。注:雖然對於每個連接@@autocommit這個這個系統變量是獨立的,但是鎖表是全局性的。所以正規操作,在一次存儲過程中調用,不管事務提交幾次,只需在整個存儲過程開端設置一次AUTOCOMMIT=0即可,在整個存儲過程結束后單獨設置一次AUTOCOMMIT=1即可。難怪我以前寫的程序會出現:
Error Code : 1205
Lock wait timeout exceeded; try restarting transaction
連接A: UPDATE e_course SET agent_nick = '222' WHERE course_id =2;
連接B:UPDATE e_course SET agent_nick = '222' WHERE course_id =3;
即使不是更新的同一行,也會超時。是因為我的存儲過程里邊連接A對commit沒有封閉那個事務,使得事務的set AUTOCOMMIT這個值被置為0,這樣,對於這個表的后續操作,則被鎖住了。如果此時直接關閉連接A,那么連接B就可以順利提交。如果在存儲過程里寫SET AUTOCOMMIT=1; 那么即使沒有commit也會解鎖。如果保險起見,在存儲過程結束應該寫上SET AUTOCOMMIT=1; 這樣可以避免此連接萬一沒有釋放會造成表鎖。注意,這個情況只對此表來說,換了其他的表,可以非常正常地更新。這種阻塞的情況,如果存儲過程修改成COMMIT; 或者是 set AUTOCOMMIT=1; 那么被阻塞的語句為立刻執行。使用START TRANSACTION,autocommit仍然被禁用,直到您使用COMMIT或ROLLBACK結束事務為止。然后autocommit模式恢復到原來的狀態。
mysql的autocommit(自動提交)默認是開啟,其對mysql的性能有一定影響,舉個例子來說,如果你插入了1000數據,mysql會commit1000次的,如果我們把autocommit關閉掉,通過程序來控制,只要一次commit就可以了。