在最近的開發中,碰到一個需求簽到,每個用戶每天只能簽到一次,那么怎么去判斷某個用戶當天是否簽到呢?因為當屬表設計的時候,每個用戶簽到一次,即向表中插入一條記錄,根據記錄的數量和時間來判斷用戶當天是否簽到。
這樣的話就會有一個問題,如果是在網速過慢的情況下,用戶多次點擊簽到按鈕,那么變會發送多次請求,可能會導致一天多次簽到,重復提交的問題,那么很自然的想到用事務。這次用的是spring + mybtais的框架,一開始設計的代碼大致如下:
public boolean signIn(SignInHistory signInHistory) { //編程式開啟事務 TransactionTemplate template = new TransactionTemplate(transactionManager); boolean result = (boolean) template.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus transactionStatus) { try { //獲取用戶所有簽到記錄 List<SignInHistory> SignInHistoryList = signInMapper.select(signInHistory); //如果當前時間和List中某條簽到時間相同,則當天已簽到,代碼略去 //插入簽到歷史表 signInMapper.insert(signInHistory); } catch (Exception e) { transactionStatus.setRollbackOnly(); logger.error(e); return false; } return true; } }); }
但是在測試中,發現還是會發生重復提交。
那么看mysql文檔
Consistent read is the default mode in which InnoDB processes SELECT statements in READ COMMITTED andREPEATABLE READ isolation levels. A consistent read does not set any locks on the tables it accesses, and therefore other sessions are free to modify those tables at the same time a consistent read is being performed on the table.
Mysql文檔中也有相關說明:如果是在read committed和repeatab read 下,普通的select語句並不會進行鎖操作。其它session可以照常更新或插入操作。
所以在這里面就可以發現,如果只是普通select,不管在不在事務中,mysql都不會將select加鎖,所以根本無法阻止其它事務插入記錄。
由此可以得出一個理解,
事務隔離級別
數據庫事務隔離級別,只是針對一個事務能不能讀取其它事務的中間結果。
Read Uncommitted(讀取未提交內容)
在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用於實際應用,因為它的性能也不比其他級別好多少。讀取未提交的數據,也被稱之為臟讀(Dirty Read)。
Read Committed(讀取提交內容)
這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別也支持所謂的不可重復讀(Nonrepeatable Read),因為同一事務的其他實例在該實例處理其間可能會有新的commit,所以同一select可能返回不同結果。
Repeatable Read(可重讀)
這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在並發讀取數據時,會看到同樣的數據行。不過理論上,這會導致另一個棘手的問題:幻讀(Phantom Read)。簡單的說,幻讀指當用戶讀取某一范圍的數據行時,另一個事務又在該范圍內插入了新行,當用戶再讀取該范圍的數據行時,會發現有新的"幻影" 行。InnoDB和Falcon存儲引擎通過多版本並發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。
Serializable(可串行化)
這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。
這四種隔離級別采取不同的鎖類型來實現,若讀取的是同一個數據的話,就容易發生問題。例如:
臟讀(Drity Read):某個事務已更新一份數據,另一個事務在此時讀取了同一份數據,由於某些原因,前一個RollBack了操作,則后一個事務所讀取的數據就會是不正確的。
不可重復讀(Non-repeatable read):在一個事務的兩次查詢之中數據不一致,這可能是兩次查詢過程中間插入了一個事務更新的原有的數據。
幻讀(Phantom Read):在一個事務的兩次查詢中數據筆數不一致,例如有一個事務查詢了幾列(Row)數據,而另一個事務卻在此時插入了新的幾列數據,先前的事務在接下來的查詢中,就會發現有幾列數據是它先前所沒有的。
事務傳播級別
數據庫事務傳播級別,指的是事務嵌套時,應該采用什么策略,即在一個事務中調用別的事務,該怎么辦
假如有一下兩個事務:
ServiceA {
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
void methodB() {
}
}
1: PROPAGATION_REQUIRED
加入當前正要執行的事務不在另外一個事務里,那么就起一個新的事務
比如說,ServiceB.methodB的事務級別定義為PROPAGATION_REQUIRED, 那么由於執行ServiceA.methodA的時候,
ServiceA.methodA已經起了事務,這時調用ServiceB.methodB,ServiceB.methodB看到自己已經運行在ServiceA.methodA
的事務內部,就不再起新的事務。而假如ServiceA.methodA運行的時候發現自己沒有在事務中,他就會為自己分配一個事務。
這樣,在ServiceA.methodA或者在ServiceB.methodB內的任何地方出現異常,事務都會被回滾。即使ServiceB.methodB的事務已經被
提交,但是ServiceA.methodA在接下來fail要回滾,ServiceB.methodB也要回滾
2: PROPAGATION_SUPPORTS
如果當前在事務中,即以事務的形式運行,如果當前不再一個事務中,那么就以非事務的形式運行
3: PROPAGATION_MANDATORY
必須在一個事務中運行。也就是說,他只能被一個父事務調用。否則,他就要拋出異常
4: PROPAGATION_REQUIRES_NEW
這個就比較繞口了。比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_REQUIRES_NEW,
那么當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的事務,等待ServiceB.methodB的事務完成以后,
他才繼續執行。他與PROPAGATION_REQUIRED 的事務區別在於事務的回滾程度了。因為ServiceB.methodB是新起一個事務,那么就是存在
兩個不同的事務。如果ServiceB.methodB已經提交,那么ServiceA.methodA失敗回滾,ServiceB.methodB是不會回滾的。如果ServiceB.methodB失敗回滾,
如果他拋出的異常被ServiceA.methodA捕獲,ServiceA.methodA事務仍然可能提交。
5: PROPAGATION_NOT_SUPPORTED
當前不支持事務。比如ServiceA.methodA的事務級別是PROPAGATION_REQUIRED ,而ServiceB.methodB的事務級別是PROPAGATION_NOT_SUPPORTED ,
那么當執行到ServiceB.methodB時,ServiceA.methodA的事務掛起,而他以非事務的狀態運行完,再繼續ServiceA.methodA的事務。
6: PROPAGATION_NEVER
不能在事務中運行。假設ServiceA.methodA的事務級別是PROPAGATION_REQUIRED,而ServiceB.methodB的事務級別是PROPAGATION_NEVER ,
那么ServiceB.methodB就要拋出異常了。
7: PROPAGATION_NESTED
理解Nested的關鍵是savepoint。他與PROPAGATION_REQUIRES_NEW的區別是,PROPAGATION_REQUIRES_NEW另起一個事務,將會與他的父事務相互獨立,
而Nested的事務和他的父事務是相依的,他的提交是要等和他的父事務一塊提交的。也就是說,如果父事務最后回滾,他也要回滾的。
而Nested事務的好處是他有一個savepoint。
行級鎖
如果有兩個事務A,B 都有 read 和write操作,如果邏輯是如果表中沒有記錄則插入,那么因為read操作並沒有加鎖,A,B進行read操作時,有可能表中都沒有記錄,那么事務A,B都會進行插入操作,表中將會有兩條記錄。
如果要保證在事務並發時,每條事務讀取到的數據都是最新的,那么只能采用鎖。
在select語句后加上 FOR UPDATE,再測試,重復提交的問題被解決了。
但是問題又來了,如果在select語句后加上LOCK IN SHARE MODE,那么會報死鎖的錯誤。
查看mysql文檔:
- SELECT ... LOCK IN SHARE MODE sets a shared mode lock on any rows that are read. Other sessions can read the rows, but cannot modify them until your transaction commits. If any of these rows were changed by another transaction that has not yet committed, your query waits until that transaction ends and then uses the latest values.
-
SELECT ... FOR UPDATE sets an exclusive lock on the rows read. An exclusive lock prevents other sessions from accessing the rows for reading or writing.
LOCK IN SHARE MODE會在讀取的行上加共享鎖,其他session只能讀不能修改或刪除,如果有其他事務修改了記錄,那么會等待事務提交后,再讀取。
FOR UPDATE在讀取行上設置一個排他鎖。阻止其他session讀取或者寫入行數據
這樣看起來似乎就能解釋為什么使用LOCK IN SHARE MODE會產生死鎖了,假如兩個事務A、B都讀取同一行記錄,那么在這一行就加上了共享鎖,但是A和B事務中都需要修改這一行,那么都要等待對方釋放共享鎖才能進行,結果造成了死鎖。
只能使用for update 來防止死鎖和重復插入。
這就是mysql的兩種行級鎖的區別。