數據庫-事務和鎖

事務

所謂事務是用戶定義的一個數據庫操做系列,這些操做要麼所有執行,要麼所有不執行,是一個不可分割的工做單位。例如在關係數據庫中,一個事務能夠是一條sql語句、一組sql語句或整個程序。html

給個栗子:sql

小IT在網上購物,其付款過程至少包括如下幾步數據庫操做:數據庫

  1. 更新客戶所購商品的庫存信息;
  2. 生成訂單而且保存到數據庫;
  3. 更新用戶相關信息,例如購物數量等;

正常狀況下,操做順利進行,最終交易成功,那麼與交易相關的全部數據庫信息也成功更新。可是,若是在這一系列過程當中任何一個環節出了差錯,例如在更新商品庫存信息時發生異常、該顧客銀行賬戶存款不足等,都將致使交易失敗。一旦交易失敗,數據庫中全部信息都必須保持交易前的狀態不變,好比最後一步更新用戶信息時失敗而致使交易失敗,那麼必須保證這筆失敗的交易不影響數據庫的狀態--庫存信息沒有被更新、用戶也沒有付款,訂單也沒有生成。不然,數據庫的信息將會一片混亂而不可預測。服務器

數據庫事務正是用來保證這種狀況下交易的平穩性和可預測性的技術。數據結構

事務的ACID特性

A(Atomicity)原子性

事務必須是原子工做單元;對於其數據修改,要麼全都執行,要麼全都不執行。一般,與某個事務關聯的操做具備共同的目標,而且是相互依賴的。若是系統只執行這些操做的一個子集,則可能會破壞事務的整體目標。原子性消除了系統處理操做子集的可能性。併發

C(Consistency)一致性

事務在完成時,必須使全部的數據都保持一致狀態。在相關數據庫中,全部規則都必須應用於事務的修改,以保持全部數據的完整性。事務結束時,全部的內部數據結構(如 B 樹索引或雙向鏈表)都必須是正確的。某些維護一致性的責任由應用程序開發人員承擔,他們必須確保應用程序已強制全部已知的完整性約束。例如,當開發用於轉賬的應用程序時,應避免在轉賬過程當中任意移動小數點。post

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的獨佔鎖而沒法上升的獨佔鎖也就不可能釋放共享鎖,因而出現了死鎖。這種死鎖比較隱蔽,但在稍大點的項目中常常發生。如在某項目中,頁面上的按鈕點擊後,沒有使按鈕馬上失效,使得用戶會屢次快速點擊同一按鈕,這樣同一段代碼對數據庫同一條記錄進行屢次操做,很容易就出現這種死鎖的狀況。 

解決方法: 
一、對於按鈕等控件,點擊後使其馬上失效,不讓用戶重複點擊,避免對同時對同一條記錄操做。 
二、使用樂觀鎖進行控制。樂觀鎖大可能是基於數據版本(Version)記錄機制實現。即爲數據增長一個版本標識,在基於數據庫表的版本解決方案中,通常是 經過爲數據庫表增長一個「version」字段來實現。讀取出數據時,將此版本號一同讀出,以後更新時,對此版本號加一。此時,將提交數據的版本數據與數 據庫表對應記錄的當前版本信息進行比對,若是提交的數據版本號大於數據庫表當前版本號,則予以更新,不然認爲是過時數據。樂觀鎖機制避免了長事務中的數據庫加鎖開銷(用戶A和用戶B操做過程當中,都沒有對數據庫數據加鎖),大大提高了大併發量下的系統總體性能表現。Hibernate 在其數據訪問引擎中內置了樂觀鎖實現。須要注意的是,因爲樂觀鎖機制是在咱們的系統中實現,來自外部系統的用戶更新操做不受咱們系統的控制,所以可能會造 成髒數據被更新到數據庫中。 
三、使用悲觀鎖進行控制。悲觀鎖大多數狀況下依靠數據庫的鎖機制實現,如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)考慮使用樂觀鎖定或使事務首先得到一個獨佔鎖定。  

衝突問題

一、髒讀

某個事務讀取的數據是另外一個事務正在處理的數據。而另外一個事務可能會回滾,形成第一個事務讀取的數據是錯誤的。

二、不可重複讀

在一個事務裏兩次讀入數據,但另外一個事務已經更改了第一個事務涉及到的數據,形成第一個事務讀入舊數據。

三、幻讀

幻讀是指當事務不是獨立執行時發生的一種現象。例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的所有數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那麼,之後就會發生操做第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺同樣。

四、更新丟失

多個事務同時讀取某一數據,一個事務成功處理好了數據,被另外一個事務寫回原值,形成第一個事務更新丟失。

鎖模式

一、共享鎖

共享鎖(S 鎖)容許併發事務在封閉式併發控制下讀取 (SELECT)資源。有關詳細信息,請參閱併發控制的類型(悲觀鎖和樂觀鎖)。資源上存在共享鎖(S鎖)時,任何其餘事務都不能修改數據。讀取操做一完成,就當即釋放資源上的共享鎖(S鎖),除非將事務隔離級別設置爲可重複讀或更高級別,或者在事務持續時間內用鎖定提示保留共享鎖(S鎖)。

二、更新鎖(U鎖)

更新鎖在共享鎖和排他鎖的結合。更新鎖意味着在作一個更新時,一個共享鎖在掃描完成符合條件的數據後可能會轉化成排他鎖。

這裏面有兩個步驟:

1) 掃描獲取Where條件時。這部分是一個更新查詢,此時是一個更新鎖。

2) 若是將執行寫入更新。此時該鎖升級到排他鎖。不然,該鎖轉變成共享鎖。

更新鎖能夠防止常見的死鎖。

三、排他鎖

排他鎖(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事務讀取數據。

相關文章
相關標籤/搜索