數據庫中的事務和鎖(樂觀、悲觀鎖,共享、排他鎖,死鎖)

併發控制: 事務和鎖的存在都是爲了更好的解決併發訪問造成的數據不一致性的的問題
樂觀鎖和悲觀鎖都是爲了解決併發控制問題, 樂觀鎖可以認爲是一種在最後提交的時候檢測衝突的手段,而悲觀鎖則是一種避免衝突的手段
樂觀鎖: 是應用系統層面和數據的業務邏輯層次上的(實際上並沒有加鎖,只不過大家一直這樣叫而已),利用程序處理併發, 它假定當某一個用戶去讀取某一個數據的時候,其他的用戶不會來訪問修改這個數據,但是在最後進行事務的提交的時候會進行版本的檢查,以判斷在該用戶的操作過程中,沒有其他用戶修改了這個數據。開銷比較小
樂觀鎖的實現大部分都是基於版本控制實現的,
除此之外,還可以通過時間戳的方式,通過提前讀取,事後對比的方式實現
寫到這裏我突然想起了,java的cuurent併發包裏的Automic 類的實現原理CAS原理(Compare and Swap), 其實也可以看做是一種樂觀鎖的實現,通過將字段定義爲volalate,(不允許在線程中保存副本,每一次讀取或者修改都要從內存區讀取,或者寫入到內存中), 通過對比應該產生的結果和實際的結果來進行保證原子操作,進行併發控制(對比和交換的正確性保證 是處理器的原子操作)。

樂觀鎖的優勢和劣勢
優勢:如果數據庫記錄始終處於悲觀鎖加鎖狀態,可以想見,如果面對幾百上千個併發,那麼要不斷的加鎖減鎖,而且用戶等待的時間會非常的長, 樂觀鎖機制避免了長事務中的數據庫加鎖解鎖開銷,大大提升了大併發量下的系統整體性能表現 所以如果系統的併發非常大的話,悲觀鎖定會帶來非常大的性能問題,所以建議就要選擇樂觀鎖定的方法, 而如果併發量不大,完全可以使用悲觀鎖定的方法。樂觀鎖也適合於讀比較多的場景。
劣勢: 但是樂觀鎖也存在着問題,只能在提交數據時才發現業務事務將要失敗,如果系統的衝突非常的多,而且一旦衝突就要因爲重新計算提交而造成較大的代價的話,樂觀鎖也會帶來很大的問題,在某些情況下,發現失敗太遲的代價會非常的大。而且樂觀鎖也無法解決髒讀的問題

同時我在思考一個問題,樂觀鎖是如何保證檢查版本,提交和修改版本是一個原子操作呢? 也就是如何保證在檢查版本的期間,沒有其他事務對其進行操作?
解決方案: 將比較,更新操作寫入到同一條SQL語句中可以解決該問題,比如 update table1 set a=1, b=2, version = version +1 where version = 1; mysql 自己能夠保障單條SQL語句的原子操作性
如果是多條SQL語句,就需要mySQL的事務通過鎖機制來保障了

悲觀鎖: 完全依賴於數據庫鎖的機制實現的,在數據庫中可以使用Repeatable Read的隔離級別(可重複讀)來實現悲觀鎖,它完全滿足悲觀鎖的要求(加鎖)
它認爲當某一用戶讀取某一數據的時候,其他用戶也會對該數據進行訪問,所以在讀取的時候就對數據進行加鎖, 在該用戶讀取數據的期間,其他任何用戶都不能來修改該數據,但是其他用戶是可以讀取該數據的, 只有當自己讀取完畢才釋放鎖。

悲觀鎖的優勢和劣勢
劣勢:開銷較大,而且加鎖時間較長,對於併發的訪問性支持不好。
優勢 : 能避免衝突的發生,

我們經常會在訪問數據庫的時候用到鎖,怎麼實現樂觀鎖和悲觀鎖呢?以hibernate爲例,可以通過爲記錄添加版本或時間戳字段來實現樂觀鎖,一旦發現出現衝突了,修改失敗就要通過事務進行回滾操作。可以用session.Lock()鎖定對象來實現悲觀鎖(本質上就是執行了SELECT * FROM t FOR UPDATE語句)

樂觀鎖和悲觀所各有優缺點,在樂觀鎖和悲觀鎖之間進行選擇的標準是:發生衝突的頻率與嚴重性。
如果衝突很少,或者衝突的後果不會很嚴重,那麼通常情況下應該選擇樂觀鎖,因爲它能得到更好的併發性,而且更容易實現。但是,如果衝突太多或者衝突的結果對於用戶來說痛苦的,那麼就需要使用悲觀策略,它能避免衝突的發生。 如果要求能夠支持高併發,那麼樂觀鎖。
其實使用樂觀鎖 高併發==高衝突, 看看你怎麼衡量了。

但是現在大多數源代碼開發者更傾向於使用樂觀鎖策略

共享鎖和排它鎖是具體的鎖,是數據庫機制上的鎖。
共享鎖(讀鎖) 在同一個時間段內,多個用戶可以讀取同一個資源,讀取的過程中數據不會發生任何變化。讀鎖之間是相互不阻塞的, 多個用戶可以同時讀,但是不能允許有人修改, 任何事務都不允許獲得數據上的排它鎖,直到數據上釋放掉所有的共享鎖
排它鎖(寫鎖) 在任何時候只能有一個用戶寫入資源,當進行寫鎖時會阻塞其他的讀鎖或者寫鎖操作,只能由這一個用戶來寫,其他用戶既不能讀也不能寫

加鎖會有粒度問題,從粒度上從大到小可以劃分爲
表鎖 開銷較小,一旦有用戶訪問這個表就會加鎖,其他用戶就不能對這個表操作了,應用程序的訪問請求遇到鎖等待的可能性比較高。
頁鎖是MySQL中比較獨特的一種鎖定級別,鎖定顆粒度介於行級鎖定與表級鎖之間,所以獲取鎖定所需要的資源開銷,以及所能提供的併發處理能力也同樣是介於上面二者之間。另外,頁級鎖定和行級鎖定一樣,會發生死鎖
行鎖 開銷較大,能具體的鎖定到表中的某一行數據,但是能更好的支持併發處理會發生死鎖

事物: 用於保證數據庫的一致性
所謂數據一致性,就是當多個用戶試圖同時訪問一個數據庫,它們的事務同時使用相同的數據時,可能會發生以下四種情況:丟失更新、髒讀、不可重複讀 和 幻讀
所謂數據完整性, 數據庫中的數據是從外界輸入的,而數據的輸入由於種種原因,會發生輸入無效或錯誤信息。保證輸入的數據符合規定,
數據完整性分爲四類:實體完整性(Entity Integrity)、域完整性(Domain Integrity)、參照完整性(Referential Integrity)、用戶定義的完整性(User-definedIntegrity)。   
 數據庫採用多種方法來保證數據完整性,包括外鍵、約束、規則和觸發器。
 
事務的ACID特性
原子性Automicity,一個事務內的所有操作,要麼全做,要麼全不做
一致性Consistency,數據庫從一個一致性狀態轉到另一個一致性狀態
獨立性(隔離性)isolation, 一個事務在執行期間,對於其他事務來說是不可見的
持久性(Durability): 事務一旦成功提交,則就會永久性的對數據庫進行了修改

隔離級別: mySQL默認的隔離級別是可重複讀
在SQL 中定義了四種隔離級別;
READ UNCOMMITED(未提交度) 事務之間的數據是相互可見的
READ COMMITED(提交讀) 大多數數據庫的默認隔離級別, 保證了不可能髒讀,但是不能保證可重複讀在這個級別裏,數據的加鎖實現是讀取都是不加鎖的,但是數據的寫入、修改和刪除是需要加鎖的
REPEATABLE READ (可重複讀) 解決了不可重複讀的問題,保證了在同一個事務之中,多次讀取相同的記錄的值的結果是一致的。 但是無法解決幻讀。這個階段的事務隔離性,在mysql中是通過基於樂觀鎖原理的多版本控制實現的。

SERIALIZABLE (可串行化讀) 最高的隔離級別,解決了幻讀 ,它會在讀取的每一行數據上都進行加鎖, 有可能導致超時和鎖爭用的問題。
它的加鎖實現是讀取的時候加共享鎖,修改刪除更新的時候加排他鎖,讀寫互斥,但是併發能力差。

隔離級別 髒讀(Dirty Read) 不可重複讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

丟失更新: 當兩個或者多個事務同時對某一數據進行更新的時候,事務B的更新可能覆蓋掉事務A的更新,導致更新丟失
解決方案:
悲觀鎖的方式: 加鎖,建議最後一步更新數據的時候加上排它鎖,不要在一開始就加鎖
執行到了最後一步更新,首先做一下加鎖的查詢確認數據有沒有沒改變,如果沒有被改變,則進行數據的更新,否則失敗。 一定要是做加鎖的查詢確認,因爲如果你不加鎖的話,有可能你在做確認的時候數據又發生了改變。
樂觀鎖的方式:使用版本控制實現

級別高低是:髒讀 < 不可重複讀 < 幻讀。所以,設置了最高級別的SERIALIZABLE_READ就不用在設置REPEATABLE_READ和READ_COMMITTED了

髒讀: 事務可以讀取未提交的數據,比如:
事務A對某一個數據data=1000 進行了修改: data = 2000, 但是還沒有提交;
事務B讀取data 得到了結果data = 2000,
由於某種原因事務A撤銷了剛纔的操作,數據data = 1000 然後提交
這時事務B讀取到的2000就是髒數據。正確的數據應該還是 1000
解決方法 : 把數據庫的事務隔離級別調整到READ_COMMITTED , 但是存在事務A與B都讀取了data,A還未完成事務,B此時修改了數據data,並提交, A又讀取了data,發現data不一致了,出現了不可重複讀。

不可重複讀 在同一個事務之中,多次讀取相同的記錄的值的結果是不一樣的,針對的是數據的修改和刪除。
事務A 讀取data = 1000, 事務還未完成;
事務B 修改了data = 2000, 修改完畢事務提交;
事務A 再次讀取data, 發現data = 2000 了,與之前的讀取不一致的
解決辦法; 把數據庫的事務隔離級別調整到 REPEATABLE READ , 讀取時候不允許其他事務修改該數據,不管數據在事務過程中讀取多少次,數據都是一致的,避免了不可重複讀問題

幻讀: 當某個事務在讀取某個範圍內的記錄的時候,另外一個事務在這個範圍內增加了一行,當前一個事務再次讀取該範圍的數據的時候就會發生幻行,. 針對的是數據的插入insert
解決方案 : 採用的是範圍鎖 RangeS RangeS_S模式,鎖定檢索範圍爲只讀 或者 把數據庫的事務隔離級別調整到SERIALIZABLE_READMySQL中InnoDB 和 XtraDB 利用(多版本併發控制)解決了幻讀問題,

加鎖協議
一次封鎖協議:因爲有大量的併發訪問,爲了預防死鎖,一般應用中推薦使用一次封鎖法,就是在方法的開始階段,已經預先知道會用到哪些數據,然後全部鎖住,在方法運行之後,再全部解鎖。這種方式可以有效的避免循環死鎖,但在數據庫中卻不適用,因爲在事務開始階段,數據庫並不知道會用到哪些數據。

兩段鎖協議 將事務分成兩個階段,加鎖階段和解鎖階段(所以叫兩段鎖)
1. 加鎖階段:在該階段可以進行加鎖操作。在對任何數據進行讀操作之前要申請並獲得S鎖(共享鎖,其它事務可以繼續加共享鎖,但不能加排它鎖),在進行寫操作之前要申請並獲得X鎖(排它鎖(只有當前數據無共享鎖,無排它鎖之後才能獲得),其它事務不能再獲得任何鎖)。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。
2. 解鎖階段:當事務釋放了一個封鎖以後,事務進入解鎖階段,在該階段只能進行解鎖操作不能再進行加鎖操作。
事務提交時(commit) 和事務回滾時(rollback)會自動的同時釋放該事務所加的insert、update、delete對應的鎖

這種方式雖然無法避免死鎖,但是兩段鎖協議可以保證事務的併發調度是串行化(串行化很重要,尤其是在數據恢復和備份的時候)的

死鎖 指兩個事務或者多個事務在同一資源上相互佔用,並請求對方所佔用的資源,從而造成惡性循環的現象。
出現死鎖的原因:
1. 系統資源不足
2. 進程運行推進的順序不當
3. 資源分配不當
產生死鎖的四個必要條件
1. 互斥條件: 一個資源只能被一個進程使用
2. 請求和保持條件:進行獲得一定資源,又對其他資源發起了請求,但是其他資源被其他線程佔用,請求阻塞,但是也不會釋放自己佔用的資源。
3. 不可剝奪條件: 指進程所獲得的資源,不可能被其他進程剝奪,只能自己釋放
4. 環路等待條件: 進程發生死鎖,必然存在着進程-資源之間的環形鏈
處理死鎖的方法: 預防,避免,檢查,解除死鎖

數據庫也會發生死鎖的現象,數據庫系統實現了各種死鎖檢測和死鎖超時機制來解除死鎖,鎖監視器進行死鎖檢測
MySQL的InnoDB處理死鎖的方式是 將持有最少行級排它鎖的事務進行回滾,相對比較簡單的死鎖回滾辦法

如何避免死鎖?
避免死鎖的核心思想是:系統對進程發出每一個資源申請進行動態檢查,並根據檢查結果決定是否分配資源,如果分配後系統可能發生死鎖,則不予分配,否則予以分配.這是一種保證系統不進入不安全或者死鎖狀態的動態策略。 什麼是不安全的狀態?系統能按某種進程推進順序( P1, P2, …, Pn),爲每個進程Pi分配其所需資源,直至滿足每個進程對資源的最大需求,使每個進程都可順序地完成。此時稱 P1, P2, …, Pn 爲安全序列。如果系統無法找到一個安全序列,則稱系統處於不安全狀態。
其實第一和第二是預防死鎖的方式,分別對應着的是破壞循環等待條件,和破壞不可剝奪條件。
第一: 加鎖順序: 對所有的資源加上序號,確保所有的線程都是按照相同的順序獲得鎖,那麼死鎖就不會發生,比如有資源 A, B,規定所有的線程只能按照A–B的方式獲取資源, 這樣就不會發生 線程1持有A,請求B,線程2持有B請求A的死鎖情況發生了
第二: 獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求,同時放棄掉自己已經成功獲得的所有資源的鎖,然後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行。
第三:死鎖的提前檢測, 很出名的就是銀行家算法。 每當一個線程獲得了鎖,會存儲在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中,當一個線程請求鎖失敗時,這個線程可以遍歷鎖的關係圖看看是否有死鎖發生。
銀行家算法: 思想:
當進程首次申請資源時,要測試該進程對資源的最大需求量,如果系統現存的資源可以滿足它的最大需求量則按當前的申請量分配資源,否則就推遲分配。當進程在執行中繼續申請資源時,先測試該進程已佔用的資源數與本次申請的資源數之和是否超過了該進程對資源的最大需求量。若超過則拒絕分配資源,若沒有超過則再測試系統現存的資源能否滿足該進程尚需的最大資源量,若能滿足則按當前的申請量分配資源,否則也要推遲分配
如何預防死鎖?
主要是通過設置某些外部條件去破壞死鎖產生的四個必要條件中的一個或者幾個。
破壞互斥條件,一般不採用,因爲資源的互斥性這個特性有時候是我們所需要的;
破壞請求和保持條件可以一次性爲一個進程或者線程分配它所需要的全部資源,這樣在後面就不會發起請求資源的情況,但是這樣資源的效率利用率很低;
破壞不可剝奪條件: 當一個已保持了某些不可剝奪資源的進程,請求新的資源而得不到滿足時,它必須釋放已經保持的所有資源,待以後需要時再重新申請,但是釋放已獲得的資源可能造成前一階段工作的失效,反覆地申請和釋放資源會增加系統開銷,降低系統吞吐量;
破壞循環等待條件: ,可釆用順序資源分配法。首先給系統中的資源編號,規定每個進程,必須按編號遞增的順序請求資源,同類資源一次申請完。也就是說,只要進程提出申請分配資源Ri,則該進程在以後的資源申請中,只能申請編號大於Ri的資源。
但是這樣的話,編號必須相對穩定,這就限制了新類型設備的增加;儘管在爲資源編號時已考慮到大多數作業實際使用這些資源的順序,但也經常會發生作業使甩資源的順序與系統規定順序不同的情況,造成資源的浪費;此外,這種按規定次序申請資源的方法,也必然會給用戶的編程帶來麻煩

InnoDB 中事務隔離性的實現:
READ COMMITED 和 REPEATABLE READ 的隔離性實現:MVCC

MVCC(多版本控制系統)的實現(目的: 實現更好的併發,可以使得大部分的讀操作不用加鎖, 但是insert,delete,update是需要加鎖的):
MVCC 只在 READ COMMITED 和 REPEATABLE READ 這兩個事務隔離性級別中使用。這是因爲MVCC 和其他兩個不兼容,READ UNCOMMITED 總是讀取最新的行,不關事務, 而Seriablizable則會對每一個讀都加共享鎖。
在InnoDB中,會在每行數據後添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(即何時被刪除)。 在實際操作中,存儲的並不是時間,而是系統的版本號每開啓一個新事務,系統的版本號就會遞增
通過MVCC,雖然每行記錄都需要額外的存儲空間,更多的行檢查工作以及一些額外的維護工作,但可以減少鎖的使用,大多數讀操作都不用加鎖,讀數據操作很簡單,性能很好,並且也能保證只會讀取到符合標準的行,也只鎖住必要行。
select (不加鎖): 滿足兩個條件的結果纔會被返回:
1. 創建版本號<= 當前事務版本號,小於意味着在該事務之前沒有其他事務對其進行修改,等於意味着事務自身對其進行了修改;
2. 刪除版本號 > 當前事務版本號 意味着刪除操作是在當前事務之後進行的,或者刪除版本未定義,意味着這一行只是進行了插入,還沒有刪除過。
INSERT ; 爲新插入的每一行保存當前事務的版本號作爲創建版本號
DELETE ; 爲刪除的行保存當前事務的版本號爲刪除版本號
UPDATE; 爲修改的每一行保存當前事務的版本號作爲創建版本號

“讀”與“讀”的區別
MySQL中的讀,和事務隔離級別中的讀,是不一樣的, 在REPEATABLE READ 級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,是不及時的數據存儲在緩存等地方的數據),不是數據庫當前的數據!這在一些對於數據的時效特別敏感的業務中,就很可能出問題。

對於這種讀取歷史數據(緩存數據)的方式,我們叫它快照讀 (snapshot read),而讀取數據庫當前版本數據的方式,叫當前讀 (current read)。很顯然,在MVCC中:
快照讀:就是select ,是不加鎖的, 在REPEATABLE READ 和READ COMMITED 級別中 select語句不加鎖。
select * from table ….;
當前讀:插入/更新/刪除操作,屬於當前讀,處理的都是當前的數據,需要加鎖。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
事務的隔離級別實際上都是定義了當前讀的級別MySQL爲了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些“當前讀”,就需要另外的模塊來解決了。(這是因爲update、insert的時候肯定要讀取數據庫中的值來與當前事務要寫入的值進行對比,看看在該事務所處理的數據在這一段時間內有沒有被其他事務所操作(就是先讀取數據庫中數據的版本號與當前的版本號做檢查)

爲了解決當前讀中的幻讀問題,MySQL事務使用了Next-Key鎖。Next-Key鎖是行鎖和GAP(間隙鎖)的合併
GAP(間隙鎖)就是在兩個數據行之間進行加鎖,防止插入操作

行鎖防止別的事務修改或刪除,解決了數據不可重複讀的問題

行鎖防止別的事務修改或刪除,GAP鎖防止別的事務新增,行鎖和GAP鎖結合形成的的Next-Key鎖共同解決了RR級別在讀數據時的幻讀問題

InnoDB 中 Serializable 的隔離性實現
Serializable級別使用的是悲觀鎖的理論, 讀加共享鎖,寫加排他鎖,讀寫互斥, 在Serializable這個級別,select語句還是會加鎖的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章