最近有同事寫了段代碼,負責建立訂單的邏輯,代碼審查時發現可能會有併發的問題。同事並不認同,他認爲他的邏輯是寫在存儲過程當中的,應該沒有問題。html
代碼的邏輯大概是(僞代碼):redis
begin transaction if 查詢到客戶存在進行中的訂單 rollback transaction if 查詢到設備存在進行中的訂單 rollback transaction 插入訂單 commit transaction
下面針對這個邏輯進行分析,爲何這個事務會出現併發問題。數據庫
首先,提出兩個問題,而後帶着問題討論事務相關的知識點,最後來解決這兩個問題並回答前文的問題。併發
第一個問題,事務是否能夠併發?分佈式
第二個問題,數據庫是怎麼隔離事務的?性能
數據庫中執行事務涉及到不少方面,包括如何處理臨界資源,如何加鎖解鎖等等。可是不管事務如何執行,都須要保證如下幾個特性:spa
原子性:全部的操做是一個邏輯單元,要麼都提交成功,要麼就都失敗;.net
一致性:只有合法的數據被寫入數據庫,不然事務回滾到最初的狀態;code
隔離性:容許多個事務同時進行,而不會破壞數據的正確性和完整性;htm
持久性:事務結束後,已經提交的結果被固化保存。
共享鎖用於非獨佔的業務,容許多個事務同時讀取鎖定的資源,可是不容許資源被更新。
select
語句時默認會被加上排他鎖,也叫獨佔鎖。顧名思義,被排他鎖鎖定的資源不會容許其餘事務進行任何操做。
insert,update,delete
時默認會被加上在更新的初始階段用於鎖定所須要的資源,防止在讀取階段使用共享鎖形成死鎖。
update
時,使用更新鎖鎖定相關資源通用的事務隔離級別有四種,SQL Server還有另外擴展出來的級別,在此很少介紹。
工做方式相似於可重複讀。但它不只會鎖定受影響的數據,還會鎖定這個範圍。這就阻止了新數據插入查詢所涉及的範圍,這種狀況能夠致使幻像讀。
像已提交讀級別那樣讀數據,但會保持共享鎖直到事務結束。
只讀取提交的數據並等待其餘事務釋放排他鎖。讀數據的共享鎖在讀操做完成後當即釋放。已提交讀是SQL Server的默認隔離級別。
在讀數據時不會檢查或使用任何鎖。所以,在這種隔離級別中可能讀取到沒有提交的數據。
第一個問題,事務是否能夠併發?
答案是確定的,數據庫中爲了提升性能,容許同時進行多個事務操做,這個事務跟發起方式無關,使用存儲過程發起,或者使用代碼發起,又或者使用普通的SQL語句發起並無什麼區別。
第二個問題,數據庫是怎麼隔離事務的?
要回答這個問題,先要理解數據庫中的鎖機制和數據庫事務隔離級別。數據庫中的鎖能夠分爲三種類型:共享鎖、獨佔鎖和更新鎖。使用不一樣級別的鎖並配合不一樣的鎖定範圍已達到不一樣的事務隔離級別並在此基礎上併發或串行執行事務。
第三個問題,爲何本文開頭的事務會存在併發問題?
由於事務的開始執行的是select
,select使用的是共享鎖,有可能併發的事務在同一時間執行select
致使同時認爲本身都是合法操做,而排隊執行後續的事務。結果致使了實際上就有可能插入重複的數據,好比只剩下一個商品,卻建立了兩個銷售訂單。
根據前文所講,使用insert,update或delete
能夠在默認事務級別人爲形成事務串行化,所以能夠在事務內部一開始都使用update
更新一條公共的數據,這樣的話同類型的事務都會串行化,而後再增長一個判斷語句,用於判斷後續的事務內容是否應該執行。這樣足以確保全部的操做都按照合理合法,惟一的缺點是可能形成性能問題。
如今分佈式的系統愈來愈多,可是再分佈的系統也會有些共享資源,好比redis或zookeeper,能夠利用redis或者zookeeper造一些分佈式的鎖(此類屬於其餘博文內容,在此再也不展開)。利用事務外部的鎖將同類型的事務作一些串行化處理,再配合事務內部的檢查機制,足以確保解決事務的併發問題。