事務
所謂事務是用戶定義的一個數據庫操作系列,這些操作要么全部執行,要么全部不執行,是一個不可分割的工作單位。例如在關系數據庫中,一個事務可以是一條sql語句、一組sql語句或整個程序。
給個栗子:
小IT在網上購物,其付款過程至少包括以下幾步數據庫操作:
- 更新客戶所購商品的庫存信息;
- 生成訂單並且保存到數據庫;
- 更新用戶相關信息,例如購物數量等;
正常情況下,操作順利進行,最終交易成功,那么與交易相關的所有數據庫信息也成功更新。但是,如果在這一系列過程中任何一個環節出了差錯,例如在更新商品庫存信息時發生異常、該顧客銀行帳戶存款不足等,都將導致交易失敗。一旦交易失敗,數據庫中所有信息都必須保持交易前的狀態不變,比如最后一步更新用戶信息時失敗而導致交易失敗,那么必須保證這筆失敗的交易不影響數據庫的狀態--庫存信息沒有被更新、用戶也沒有付款,訂單也沒有生成。否則,數據庫的信息將會一片混亂而不可預測。
數據庫事務正是用來保證這種情況下交易的平穩性和可預測性的技術。
事務的ACID特性
A(Atomicity)原子性
事務必須是原子工作單元;對於其數據修改,要么全都執行,要么全都不執行。通常,與某個事務關聯的操作具有共同的目標,並且是相互依賴的。如果系統只執行這些操作的一個子集,則可能會破壞事務的總體目標。原子性消除了系統處理操作子集的可能性。
C(Consistency)一致性
事務在完成時,必須使所有的數據都保持一致狀態。在相關數據庫中,所有規則都必須應用於事務的修改,以保持所有數據的完整性。事務結束時,所有的內部數據結構(如 B 樹索引或雙向鏈表)都必須是正確的。某些維護一致性的責任由應用程序開發人員承擔,他們必須確保應用程序已強制所有已知的完整性約束。例如,當開發用於轉帳的應用程序時,應避免在轉帳過程中任意移動小數點。
I(Isolation)隔離性
指的是在並發環境中,當不同的事務同時操縱相同的數據時,每個事務都有各自的完整數據空間。由並發事務所做的修改必須與任何其他並發事務所做的修改隔離。事務查看數據更新時,數據所處的狀態要么是另一事務修改它之前的狀態,要么是另一事務修改它之后的狀態,事務不會查看到中間狀態的數據。
D(Durability)持久性
指的是只要事務成功結束,它對數據庫所做的更新就必須永久保存下來。即使發生系統崩潰,重新啟動數據庫系統后,數據庫還能恢復到事務成功結束時的狀態。
事務的ACID特性是由關系數據庫管理系統(RDBMS,數據庫系統)來實現的。數據庫管理系統采用日志來保證事務的原子性、一致性和持久性。日志記錄了事務對數據庫所做的更新,如果某個事務在執行過程中發生錯誤,就可以根據日志,撤銷事務對數據庫已做的更新,使數據庫退回到執行事務前的初始狀態。數據庫管理系統采用鎖機制來實現事務的隔離性。當多個事務同時更新數據庫中相同的數據時,只允許持有鎖的事務更新數據,其他事務必須等待,直到前一個事務釋放了鎖,其他事務才有機會更新該數據。
完整的事務結構
BEGIN a transaction;//設置事務的起始點 COMMIT a transaction;//提交事務,使事務提交的數據成為持久不可更改的部分 ROLLBACK a transaction;//撤銷一個事務,回滾,使之成為事務開始前的狀態 SAVE a transaction;//建立標簽,用作部分回滾,使之恢復到標簽初的狀態
事務的語法
BEGIN TRAN[SACTION] [<transaction_name>|<@transaction variable>][WITH MARK['<description>']][;] COMMIT [TRAN[SACTION][<transaction_name>|<@transaction varible>]][;] ROLLBACK TRAN[SACRION][<transaction name>|<save point name>|<@transaction varible>|<@save point varible>][;] SAVE TRAN[SACTION][<save point name>|<@svae point varible>][;]
"[]"里面是需補充的部分。
給個栗子:
下面整個示例,先來建一張表(使用SqlServer)如下:
BEGIN TRAN Tran_Money --開始事務 DECLARE @tran_error int; SET @tran_error = 0; BEGIN TRY UPDATE tb_Money SET MyMoney = MyMoney - 30 WHERE Name = '劉備'; SET @tran_error = @tran_error + @@ERROR; --測試出錯代碼,看看劉備的錢減少,關羽的錢是否會增加 --SET @tran_error = 1; UPDATE tb_Money SET MyMoney = MyMoney + 30 WHERE Name = '關羽'; SET @tran_error = @tran_error + @@ERROR; END TRY BEGIN CATCH PRINT '出現異常,錯誤編號:' + convert(varchar,error_number()) + ',錯誤消息:' + error_message() SET @tran_error = @tran_error + 1 END CATCH IF(@tran_error > 0) BEGIN --執行出錯,回滾事務 ROLLBACK TRAN; PRINT '轉賬失敗,取消交易!'; END ELSE BEGIN --沒有異常,提交事務 COMMIT TRAN; PRINT '轉賬成功!'; END
本栗子來源於SQL Server 事務語法
鎖
數據庫和操作系統一樣,是一個多用戶使用的共享資源。當多個用戶並發地存取數據時,在數據庫中就會產生多個事務同時存取同一數據的情況。若對並發操作不加控制就可能會讀取和存儲不正確的數據,破壞數據庫的一致性。加鎖是實現數據庫並 發控制的一個非常重要的技術。在實際應用中經常會遇到的與鎖相關的異常情況,當兩個事務需要一組有沖突的鎖,而不能將事務繼續下去的話,就會出現死鎖,嚴重影響應用的正常執行。
在數據庫中有兩種基本的鎖類型:排它鎖(Exclusive Locks,即X鎖)和共享鎖(Share Locks,即S鎖)。當數據對象被加上排它鎖時,其他的事務不能對它讀取和修改。加了共享鎖的數據對象可以被其他事務讀取,但不能修改。數據庫利用這兩 種基本的鎖類型來對數據庫的事務進行並發控制。
死鎖的幾種情況
死鎖的第一種情況
一個用戶A 訪問表A(鎖住了表A),然后又訪問表B;另一個用戶B 訪問表B(鎖住了表B),然后企圖訪問表A;這時用戶A由於用戶B已經鎖住表B,它必須等待用戶B釋放表B才能繼續,同樣用戶B要等用戶A釋放表A才能繼續,這就死鎖就產生了。
解決方法:
這種死鎖比較常見,是由於程序的BUG產生的,除了調整的程序的邏輯沒有其它的辦法。仔細分析程序的邏輯,對於數據庫的多表操作時,盡量按照相同的順序進 行處理,盡量避免同時鎖定兩個資源,如操作A和B兩張表時,總是按先A后B的順序處理, 必須同時鎖定兩個資源時,要保證在任何時刻都應該按照相同的順序來鎖定資源。
死鎖的第二種情況
用戶A查詢一條紀錄,然后修改該條紀錄;這時用戶B修改該條紀錄,這時用戶A的事務里鎖的性質由查詢的共享鎖企圖上升到獨占鎖,而用戶B里的獨占鎖由於A 有共享鎖存在所以必須等A釋放掉共享鎖,而A由於B的獨占鎖而無法上升的獨占鎖也就不可能釋放共享鎖,於是出現了死鎖。這種死鎖比較隱蔽,但在稍大點的項目中經常發生。如在某項目中,頁面上的按鈕點擊后,沒有使按鈕立刻失效,使得用戶會多次快速點擊同一按鈕,這樣同一段代碼對數據庫同一條記錄進行多次操作,很容易就出現這種死鎖的情況。
解決方法:
1、對於按鈕等控件,點擊后使其立刻失效,不讓用戶重復點擊,避免對同時對同一條記錄操作。
2、使用樂觀鎖進行控制。樂觀鎖大多是基於數據版本(Version)記錄機制實現。即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是 通過為數據庫表增加一個“version”字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數 據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。樂觀鎖機制避免了長事務中的數據庫加鎖開銷(用戶A和用戶B操作過程中,都沒有對數據庫數據加鎖),大大提升了大並發量下的系統整體性能表現。Hibernate 在其數據訪問引擎中內置了樂觀鎖實現。需要注意的是,由於樂觀鎖機制是在我們的系統中實現,來自外部系統的用戶更新操作不受我們系統的控制,因此可能會造 成臟數據被更新到數據庫中。
3、使用悲觀鎖進行控制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,如Oracle的Select … for update語句,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。如一個金融系統, 當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶賬戶余額),如果采用悲觀鎖機制,也就意味着整個操作過程中(從操作員讀 出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對成百上千個並發,這樣的情況將導致災難性的后果。所以,采用悲觀鎖進行控制時一定要考慮清楚。
死鎖的第三種情況
如果在事務中執行了一條不滿足條件的update語句,則執行全表掃描,把行級鎖上升為表級鎖,多個這樣的事務執行后,就很容易產生死鎖和阻塞。類似的情 況還有當表中的數據量非常龐大而索引建的過少或不合適的時候,使得經常發生全表掃描,最終應用系統會越來越慢,最終發生阻塞或死鎖。
解決方法:
SQL語句中不要使用太復雜的關聯多表的查詢;使用“執行計划”對SQL語句進行分析,對於有全表掃描的SQL語句,建立相應的索引進行優化。
總體上來說,產生內存溢出與鎖表都是由於代碼寫的不好造成的,因此提高代碼的質量是最根本的解決辦法。有的人認為先把功能實現,有BUG時再在測試階段進 行修正,這種想法是錯誤的。正如一件產品的質量是在生產制造的過程中決定的,而不是質量檢測時決定的,軟件的質量在設計與編碼階段就已經決定了,測試只是對軟件質量的一個驗證,因為測試不可能找出軟件中所有的BUG。
如何避免死鎖
1 使用事務時,盡量縮短事務的邏輯處理過程,及早提交或回滾事務;
2 設置死鎖超時參數為合理范圍,如:3分鍾-10分種;超過時間,自動放棄本次操作,避免進程懸掛;
3 所有的SP都要有錯誤處理(通過@error)
4 一般不要修改SQL SERVER事務的默認級別。不推薦強行加鎖
5 優化程序,檢查並避免死鎖現象出現;
1)合理安排表訪問順序
2)在事務中盡量避免用戶干預,盡量使一個事務處理的任務少些。
3)采用臟讀技術。臟讀由於不對被訪問的表加鎖,而避免了鎖沖突。在客戶機/服務器應用環境中,有些事務往往不允許讀臟數據,但在特定的條件下,我們可以用臟讀。
4)數據訪問時域離散法。數據訪問時域離散法是指在客戶機/服務器結構中,采取各種控制手段控制對數據庫或數據庫中的對象訪問時間段。主要通過以下方式實 現: 合理安排后台事務的執行時間,采用工作流對后台事務進行統一管理。工作流在管理任務時,一方面限制同一類任務的線程數(往往限制為1個),防止資源過多占 用; 另一方面合理安排不同任務執行時序、時間,盡量避免多個后台任務同時執行,另外,避免在前台交易高峰時間運行后台任務
5)數據存儲空間離散法。數據存儲空間離散法是指采取各種手段,將邏輯上在一個表中的數據分散到若干離散的空間上去,以便改善對表的訪問性能。主要通過以下方法實現: 第一,將大表按行或列分解為若干小表; 第二,按不同的用戶群分解。
6)使用盡可能低的隔離性級別。隔離性級別是指為保證數據庫數據的完整性和一致性而使多用戶事務隔離的程度,SQL92定義了4種隔離性級別:未提交讀、 提交讀、可重復讀和可串行。如果選擇過高的隔離性級別,如可串行,雖然系統可以因實現更好隔離性而更大程度上保證數據的完整性和一致性,但各事務間沖突而死鎖的機會大大增加,大大影響了系統性能。
7)使用Bound Connections。Bound connections 允許兩個或多個事務連接共享事務和鎖,而且任何一個事務連接要申請鎖如同另外一個事務要申請鎖一樣,因此可以允許這些事務共享數據而不會有加鎖的沖突。
8)考慮使用樂觀鎖定或使事務首先獲得一個獨占鎖定。
沖突問題
1、臟讀
某個事務讀取的數據是另一個事務正在處理的數據。而另一個事務可能會回滾,造成第一個事務讀取的數據是錯誤的。
2、不可重復讀
在一個事務里兩次讀入數據,但另一個事務已經更改了第一個事務涉及到的數據,造成第一個事務讀入舊數據。
3、幻讀
幻讀是指當事務不是獨立執行時發生的一種現象。例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那么,以后就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。
4、更新丟失
多個事務同時讀取某一數據,一個事務成功處理好了數據,被另一個事務寫回原值,造成第一個事務更新丟失。
鎖模式
1、共享鎖
共享鎖(S 鎖)允許並發事務在封閉式並發控制下讀取 (SELECT)資源。有關詳細信息,請參閱並發控制的類型(悲觀鎖和樂觀鎖)。資源上存在共享鎖(S鎖)時,任何其他事務都不能修改數據。讀取操作一完成,就立即釋放資源上的共享鎖(S鎖),除非將事務隔離級別設置為可重復讀或更高級別,或者在事務持續時間內用鎖定提示保留共享鎖(S鎖)。
2、更新鎖(U鎖)
更新鎖在共享鎖和排他鎖的結合。更新鎖意味着在做一個更新時,一個共享鎖在掃描完成符合條件的數據后可能會轉化成排他鎖。
這里面有兩個步驟:
1) 掃描獲取Where條件時。這部分是一個更新查詢,此時是一個更新鎖。
2) 如果將執行寫入更新。此時該鎖升級到排他鎖。否則,該鎖轉變成共享鎖。
更新鎖可以防止常見的死鎖。
3、排他鎖
排他鎖(X 鎖)可以防止並發事務對資源進行訪問。排他鎖不與其他任何鎖兼容。使用排他鎖(X鎖)時,任何其他事務都無法修改數據;僅在使用 NOLOCK提示或未提交讀隔離級別時才會進行讀取操作。
悲觀鎖
悲觀鎖是指假設並發更新沖突會發生,所以不管沖突是否真的發生,都會使用鎖機制。
悲觀鎖會完成以下功能:鎖住讀取的記錄,防止其它事務讀取和更新這些記錄。其它事務會一直阻塞,直到這個事務結束.
悲觀鎖是在使用了數據庫的事務隔離功能的基礎上,獨享占用的資源,以此保證讀取數據一致性,避免修改丟失。
悲觀鎖可以使用Repeatable Read事務,它完全滿足悲觀鎖的要求。
樂觀鎖
樂觀鎖不會鎖住任何東西,也就是說,它不依賴數據庫的事務機制,樂觀鎖完全是應用系統層面的東西。
如果使用樂觀鎖,那么數據庫就必須加版本字段,否則就只能比較所有字段,但因為浮點類型不能比較,所以實際上沒有版本字段是不可行的。
事務隔離級別
數據庫事務的隔離級別有4個,由低到高依次為Read uncommitted、Read committed、Repeatable read、Serializable,這四個級別可以逐個解決臟讀、不可重復讀、幻讀這幾類問題。
READ UNCOMMITTED-讀未提交
Read UnCommitted事務可以讀取事務已修改,但未提交的的記錄。
Read UnCommitted事務會產生臟讀(Dirty Read)。
Read UnCommitted事務與select語句加nolock的效果一樣,它是所有隔離級別中限制最少的。
本栗子來源於數據庫事務隔離級別
公司發工資了,領導把5000元打到singo的賬號上,但是該事務並未提交,而singo正好去查看賬戶,發現工資已經到賬,是5000元整,非常高興。可是不幸的是,領導發現發給singo的工資金額不對,是2000元,於是迅速回滾了事務,修改金額后,將事務提交,最后singo實際的工資只有2000元,singo空歡喜一場。
出現上述情況,即我們所說的臟讀,兩個並發的事務,“事務A:領導給singo發工資”、“事務B:singo查詢工資賬戶”,事務B讀取了事務A尚未提交的數據。
當隔離級別設置為Read uncommitted時,就可能出現臟讀,如何避免臟讀,請看下一個隔離級別。
READ COMMITTED-讀提交
一旦創建共享鎖的語句執行完成,該鎖頂便釋放。
Read Committed是SQL Server的預設隔離等級。
Read Committed只可以防止臟讀。
--先創建表: CREATE TABLE tb(id int,val int) INSERT tb VALUES(1,10) INSERT tb VALUES(2,20) 然后在連接1中,執行: SET TRANSACTION ISOLATION LEVEL READ COMMITTED BEGIN TRANSACTION SELECT * FROM tb; --這個SELECT結束后,就會釋放掉共享鎖 WAITFOR DELAY '00:00:05' --模擬事務處理,等待5秒 SELECT * FROM tb; --再次SELECT tb表 ROLLBACK --回滾事務 在連接2中,執行 UPDATE tb SET val = val + 10 WHERE id = 2; -------- 回到連接1中.可以看到.兩次SELECT的結果是不同的. 因為在默認的READ COMMITTED隔離級別下,SELECT完了.就會馬上釋放掉共享鎖.
singo拿着工資卡去消費,系統讀取到卡里確實有2000元,而此時她的老婆也正好在網上轉賬,把singo工資卡的2000元轉到另一賬戶,並在 singo之前提交了事務,當singo扣款時,系統檢查到singo的工資卡已經沒有錢,扣款失敗,singo十分納悶,明明卡里有錢,為何......
出現上述情況,即我們所說的不可重復讀 ,兩個並發的事務,“事務A:singo消費”、“事務B:singo的老婆網上轉賬”,事務A事先讀取了數據,事務B緊接了更新了數據,並提交了事務,而事務A再次讀取該數據時,數據已經發生了改變。
當隔離級別設置為Read committed 時,避免了臟讀,但是可能會造成不可重復讀。
大多數數據庫的默認級別就是Read committed,比如Sql Server , Oracle。如何解決不可重復讀這一問題,請看下一個隔離級別。
REPEATABLE READ-重復讀
REPEATABLE READ事務不會產生臟讀,並且在事務完成之前,任何其它事務都不能修改目前事務已讀取的記錄。
其它事務仍可以插入新記錄,但必須符合當前事務的搜索條件——這意味着當前事務重新查詢記錄時,會產生幻讀(Phantom Read)。
當隔離級別設置為Repeatable read 時,可以避免不可重復讀。當singo拿着工資卡去消費時,一旦系統開始讀取工資卡信息(即事務開始),singo的老婆就不可能對該記錄進行修改,也就是singo的老婆不能在此時轉賬。
雖然Repeatable read避免了不可重復讀,但還有可能出現幻讀 。
singo的老婆工作在銀行部門,她時常通過銀行內部系統查看singo的信用卡消費記錄。有一天,她正在查詢到singo當月信用卡的總消費金額 (select sum(amount) from transaction where month = 本月)為80元,而singo此時正好在外面胡吃海塞后在收銀台買單,消費1000元,即新增了一條1000元的消費記錄(insert transaction ... ),並提交了事務,隨后singo的老婆將singo當月信用卡消費的明細打印到A4紙上,卻發現消費總額為1080元,singo的老婆很詫異,以為出 現了幻覺,幻讀就這樣產生了。
注:Mysql的默認隔離級別就是Repeatable read。
SERIALIZABLE-序列化
SERIALIZABLE可以防止除更新丟失外所有的一致性問題,即:
1.語句無法讀取其它事務已修改但未提交的記錄。
2.在當前事務完成之前,其它事務不能修改目前事務已讀取的記錄。
3.在當前事務完成之前,其它事務所插入的新記錄,其索引鍵值不能在當前事務的任何語句所讀取的索引鍵范圍中。
SNAPSHOT
Snapshot事務中任何語句所讀取的記錄,都是事務啟動時的數據。
這相當於事務啟動時,數據庫為事務生成了一份專用“快照”。在當前事務中看到不其它事務在當前事務啟動之后所進行的數據修改。
Snapshot事務不會讀取記錄時要求鎖定,讀取記錄的Snapshot事務不會鎖住其它事務寫入記錄,寫入記錄的事務也不會鎖住Snapshot事務讀取數據。