概要:
事務的四個特性:原子性、一致性、隔離性、持久性
事務不隔離帶來的問題:更新丟失、臟讀、不可重復讀、虛讀(幻讀)。其中更新丟失就是並發寫,這是一定不允許的,因此一定要解決更新丟失問題。
事務隔離的級別:讀未提交(1000)、讀已提交(1100)、可重復讀(1110)、串行化(1111)。
|
|
更新丟失 |
臟讀 |
不可重復讀 |
幻讀 |
| RU(讀未提交) |
避免 |
|
|
|
| RC(讀提交) |
避免 |
避免 |
|
|
| RR(可重復讀) |
避免 |
避免 |
避免 |
|
| S(串行化) |
避免 |
避免 |
避免 |
避免
|
1、事務四個特性
如果一個數據庫聲稱支持事務的操作,那么該數據庫必須要具備以下四個特性:
(1)原子性(Atomicity)
原子性是指事務包含的所有操作要么全部成功,要么全部失敗回滾,這和前面兩篇博客介紹事務的功能是一樣的概念,因此事務的操作如果成功就必須要完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。
(2)一致性(Consistency)
一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之后都必須處於一致性狀態。
拿轉賬來說,假設用戶A和用戶B兩者的錢加起來一共是5000,那么不管A和B之間如何轉賬,轉幾次賬,事務結束后兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。
(3)隔離性(Isolation)
隔離性是當多個用戶並發訪問數據庫時,比如操作同一張表時,數據庫為每一個用戶開啟的事務,不能被其他事務的操作所干擾,多個並發事務之間要相互隔離。
即要達到這么一種效果:對於任意兩個並發的事務T1和T2,在事務T1看來,T2要么在T1開始之前就已經結束,要么在T1結束之后才開始,這樣每個事務都感覺不到有其他事務在並發地執行。
關於事務的隔離性數據庫提供了多種隔離級別,稍后會介紹到。
(4)持久性(Durability)
持久性是指一個事務一旦被提交了,那么對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。
例如我們在使用JDBC操作數據庫時,在提交事務方法后,提示用戶事務操作完成,當我們程序執行完成直到看到提示后,就可以認定事務以及正確提交,即使這時候數據庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是數據庫因為故障而沒有執行事務的重大錯誤。
2、事務隔離
以上介紹完事務的四大特性(簡稱ACID),現在重點來說明下事務的隔離性,當多個線程都開啟事務操作數據庫中的數據時,數據庫系統要能進行隔離操作,以保證各個線程獲取數據的准確性。
2.1、不用事務隔離帶來的問題
在介紹數據庫提供的各種隔離級別之前,我們先看看如果不考慮事務的隔離性,會發生的幾種問題:
更新丟失:兩事務同時更新,一個失敗回滾覆蓋另一個事務的更新。或事務1執行更細操作,在事務1結束前事務2也更新,則事務1的更細結果被事務2的覆蓋了。
臟讀:事務T2讀取到事務T1修改了但是還未提交的數據,之后事務T1又回滾其更新操作,導致事務T2讀到的是臟數據。
不可重復讀:事務T1讀取某個數據后,事務T2對其做了修改,當事務T1再次讀該數據時得到與前一次不同的值。
虛讀(幻讀):事務T1讀取在讀取某范圍數據時,事務T2又插入一條數據,當事務T1再次數據這個范圍數據時發現不一樣了,出現了一些“幻影行”。
臟讀和不可重復讀的區別:臟讀是某一事務讀取了另一個事務未提交的臟數據,而不可重復讀則是讀取了前一事務提交的數據。
不可重復讀和幻讀的異同:都是讀取了另一條已經提交的事務(這點就臟讀不同),所不同的是不可重復讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。
(1)更新丟失
1、更新丟失(Lostupdate)
兩個事務同時更新,第二個事務回滾會覆蓋第一個事務更新的數據,導致更新丟失
2、兩次更新問題(Secondlost updates problem)
兩個事務都讀取了數據,並同時更新,第一個事務更新失敗,因為被第二個事務覆蓋。
(2)臟讀
臟讀是指在一個事務處理過程里讀取了另一個未提交的事務中的數據。
當一個事務正在多次修改某個數據,而在這個事務中這多次的修改都還未提交,這時一個並發的事務來訪問該數據,就會造成兩個事務得到的數據不一致。例如:用戶A向用戶B轉賬100元,對應SQL命令如下:
update account set money=money+100 where name=’B’; (此時A通知B) update account set money=money - 100 where name=’A’;
當只執行第一條SQL時,A通知B查看賬戶,B發現確實錢已到賬(此時即發生了臟讀),而之后無論第二條SQL是否執行,只要該事務不提交,則所有操作都將回滾,那么當B以后再次查看賬戶時就會發現錢其實並沒有轉。
(3)不可重復讀
不可重復讀是指在對於數據庫中的某個數據,一個事務范圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。
例如事務T1在讀取某一數據,而事務T2立馬修改了這個數據並且提交事務給數據庫,事務T1再次讀取該數據就得到了不同的結果,發送了不可重復讀。
不可重復讀和臟讀的區別是,臟讀是某一事務讀取了另一個事務未提交的臟數據,而不可重復讀則是讀取了前一事務提交的數據。
在某些情況下,不可重復讀並不是問題,比如我們多次查詢某個數據當然以最后查詢得到的結果為主。但在另一些情況下就有可能發生問題,例如對於同一個數據A和B依次查詢就可能不同,A和B就可能打起來了……
(4)虛讀(幻讀)
幻讀是事務非獨立執行時發生的一種現象。例如事務T1對一個表中所有的行的某個數據項做了從“1”修改為“2”的操作,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值還是為“1”並且提交給數據庫。而操作事務T1的用戶如果再查看剛剛修改的數據,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀。
幻讀和不可重復讀的異同:都是讀取了另一條已經提交的事務(這點就臟讀不同),所不同的是不可重復讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。
2.2、事務隔離的級別
為此我們需要通過提供不同類型的“鎖”機制針對數據庫事務進行不同程度的並發訪問控制,由此產生了不同的事務隔離級別:隔離級別(低->高)。SQL、SQL2標准定義了四種隔離級別:
●讀未提交(Read Uncommitted)
含義解釋:只限制同一數據寫事務禁止其他寫事務。解決”更新丟失”。(一事務寫時禁止其他事務寫)
名稱解釋:可讀取未提交數據
所需的鎖:排他寫鎖
●讀提交(Read Committed)
含義解釋:只限制同一數據寫事務禁止其它讀寫事務。解決”臟讀”,以及”更新丟失”。(一事務寫時禁止其他事務讀寫)
名稱解釋:必須提交以后的數據才能被讀取
所需的鎖:排他寫鎖、瞬間共享讀鎖
●可重復讀(Repeatable Read)
含義解釋:限制同一數據寫事務禁止其他讀寫事務,讀事務禁止其它寫事務(允許讀)。解決”不可重復讀”,以及”更新丟失”和”臟讀”。(一事務寫時禁止其他事務讀寫、一事務讀時禁止其他事務寫)
注意沒有解決幻讀,解決幻讀的方法是增加范圍鎖(range lock)或者表鎖。
名稱解釋:能夠重復讀取
所需的鎖:排他寫鎖、共享讀鎖
●串行化(Serializable)
含義解釋:限制所有讀寫事務都必須串行化實行。它要求事務序列化執行,事務只能一個接着一個地執行,但不能並發執行。如果僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。(一事務寫時禁止其他事務讀寫、一事務讀時禁止其他事務讀寫)
所須的鎖:范圍鎖或表鎖
下表是各隔離級別對各種異常的控制能力。
|
|
更新丟失 |
臟讀 |
不可重復讀 |
幻讀 |
| RU(讀未提交) |
避免 |
|
|
|
| RC(讀提交) |
避免 |
避免 |
|
|
| RR(可重復讀) |
避免 |
避免 |
避免 |
|
| S(串行化) |
避免 |
避免 |
避免 |
避免
|
以上四種隔離級別最高的是Serializable級別,最低的是Read uncommitted級別,當然級別越高,數據完整性越好,但執行效率就越低。像Serializable這樣的級別,就是以鎖表的方式(類似於Java多線程中的鎖)使得其他的線程只能在鎖外等待,所以平時選用何種隔離級別應該根據實際情況。
2.3、常見數據庫的事務隔離
| 數據庫 |
默認級別 |
| MySQL |
可重復讀(Repeatable Read) |
| Oracle |
讀提交(Read Committed) |
| SQLServer |
讀提交(Read Committed) |
| DB2 |
讀提交(Read Committed) |
| PostgreSQL |
讀提交(Read Committed)
|
在MySQL數據庫中,支持上面四種隔離級別,默認的為Repeatable read (可重復讀);此外,MySQL的Repeatable Read隔離級別也解決了幻讀問題(通過Next-key lock加鎖方法即范圍鎖解決不可重復讀和幻讀問題,如select * from t where a>10會對key為[10,infinite)范圍的行加鎖,這樣其他事務就不能對此范圍內key對應的行更改)達到了SQL、SQL2標准中的Serializable級別。
在Oracle數據庫中,只支持Serializable (串行化)級別和Read committed (讀已提交)這兩種級別,其中默認的為Read committed級別。
在MySQL數據庫中查看當前事務的隔離級別:
select @@tx_isolation;
在MySQL數據庫中設置事務的隔離 級別:
set [glogal | session] transaction isolation level 隔離級別名稱; //設置全部連接或當前連接的事務隔離級別 set tx_isolation=’隔離級別名稱; //設置當前連接的事務隔離級別
例1:查看當前事務的隔離級別:

例2:將事務的隔離級別設置為Read uncommitted級別:

或:

記住:設置數據庫的隔離級別一定要是在開啟事務之前!
如果是使用JDBC對數據庫的事務設置隔離級別的話,也應該是在調用Connection對象的setAutoCommit(false)方法之前。調用Connection對象的setTransactionIsolation(level)即可設置當前鏈接的隔離級別,至於參數level,可以使用Connection對象的字段:

在JDBC中設置隔離級別的部分代碼:

后記:隔離級別的設置只對當前鏈接有效。對於使用MySQL命令窗口而言,一個窗口就相當於一個鏈接,當前窗口設置的隔離級別只對當前窗口中的事務有效;對於JDBC操作數據庫來說,一個Connection對象相當於一個鏈接,而對於Connection對象設置的隔離級別只對該Connection對象有效,與其他鏈接Connection對象無關。
3、事務傳播
事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。以Spring Transaction為例,在TransactionDefinition接口定義中包括了如下幾個表示傳播行為的常量:
- TransactionDefinition.PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。這是默認值。
- TransactionDefinition.PROPAGATION_REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
- TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
- TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
- TransactionDefinition.PROPAGATION_NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED。
參考資料:
1、http://www.cnblogs.com/fjdingsd/p/5273008.html
2、http://www.2cto.com/database/201603/495078.html
