原文地址:https://my.oschina.net/oosc/blog/1620279
前言
鎖是防止在兩個事務操作同一個數據源(表或行)時交互破壞數據的一種機制。
數據庫采用封鎖技術保證並發操作的可串行性。
以Oracle為例:
Oracle的鎖分為兩大類:數據鎖(也稱DML鎖)和字典鎖。
字典鎖是Oracle DBMS內部用於對字典表的封鎖。
字典鎖包括語法分析鎖和DDL鎖,由DBMS在必要的時候自動加鎖和釋放鎖,用戶無機控制。
Oracle主要提供了5種數據鎖:
共享鎖(Share Table Lock,簡稱S鎖)、
排它鎖(Exclusive Table Lock,簡稱X鎖)、
行級鎖(Row Share Table Lock,簡稱RS鎖)、
行級排它鎖(Row Exclusive Table Lock,簡稱RX鎖)和
共享行級排它鎖(Share Row Exclusive Table Lock,簡稱SRX鎖)。
其封鎖粒度包括行級和表級。
以Mysql為例:
行級鎖/字段鎖/表級鎖
針對鎖粒度划分:行鎖、字段鎖、表鎖、庫鎖
(1)行鎖:訪問數據庫的時候,鎖定整個行數據,防止並發錯誤。
(2)字段鎖:訪問數據庫的時候,鎖定表的某幾個字段數據,防止並發錯誤。
(3)表鎖:訪問數據庫的時候,鎖定整個表數據,防止並發錯誤。
行鎖 和 表鎖 的區別:
- 表鎖: 開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖沖突概率高,並發度最低
- 行鎖: 開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖沖突的概率低,並發度高
由淺入深舉例說明:
1) 創建測試表
SYS@ORA11GR2>create table t_lock as select rownum as id,0 as type from dual connect by rownum <=3; Table created. SYS@ORA11GR2>select * from t_lock; ID TYPE ---------- ---------- 1 0 2 0 3 0
2) 會話1:查詢type為0的最小id
SYS@ORA11GR2>set time on; 18:58:22 SYS@ORA11GR2> 18:58:23 SYS@ORA11GR2>select min(id) from t_lock where type=0; MIN(ID) ---------- 1
3) 會話2:查詢type為0的最小id
SYS@ORA11GR2>set time on 18:59:31 SYS@ORA11GR2>select min(id) from t_lock where type=0; MIN(ID) ---------- 1
4) 會話1:將ID為1的這條記錄的type置為1
19:00:53 SYS@ORA11GR2>update t_lock set type=1 where id=1; 1 row updated. 19:01:21 SYS@ORA11GR2>commit; Commit complete. 19:01:37 SYS@ORA11GR2>select * from t_lock; ID TYPE ---------- ---------- 1 1 2 0 3 0
5) 會話2:將ID為1的這條記錄的type置為2
19:02:47 SYS@ORA11GR2>update t_lock set type=2 where id=1; 1 row updated. 19:03:11 SYS@ORA11GR2>commit; Commit complete. 19:03:17 SYS@ORA11GR2>select * from t_lock; ID TYPE ---------- ---------- 1 2 2 0 3 0
6) 小結:
我們看到id為1的type現在的值為2,會話1將type更新為1的記錄已經“丟失”
1.2. 悲觀鎖
1) 會話1:查詢id為2的記錄並進行鎖定
19:05:43 SYS@ORA11GR2>select * from t_lock where id=2 and type =0for update nowait; ID TYPE ---------- ---------- 2 0
2) 會話2:查詢id為2的記錄,此時查詢報錯
19:07:43 SYS@ORA11GR2>select * from t_lock where id=2 and type=0for update nowait; select * from t_lock where id=2 and type=0 for update nowait * ERROR at line 1: ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
3) 會話1:對id為2的記錄進行更新。
19:19:08 SYS@ORA11GR2>update t_lock set type=1 where id=2 and type=0; 1 row updated. 19:19:30 SYS@ORA11GR2>commit; Commit complete. 19:19:39 SYS@ORA11GR2>select * from t_lock where id=2; ID TYPE ---------- ---------- 2 1
4) 會話2:查詢id為2的記錄,由於已經將id為2的type已經變為1,所以查不到數據了。
19:19:15 SYS@ORA11GR2>select * from t_lock where id=2 and type=0for update nowait; no rows selected
1.3樂觀鎖
1) 會話1:查詢id為3的偽列ora_rowscn的值
19:22:00 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3; ID TYPE ORA_ROWSCN ---------- ---------- ---------- 3 0 1246809
2) 會話2:查詢id為3的偽列ora_rowscn的值
19:23:01 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3; ID TYPE ORA_ROWSCN ---------- ---------- ---------- 3 0 1246809
3) 會話1:更新id為3的type為1
19:24:22 SYS@ORA11GR2>update t_lock set type=1 where ora_rowscn=1246809 and id = 3; 1 row updated. 19:25:29 SYS@ORA11GR2>commit; Commit complete.
驗證:
19:28:22 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3; ID TYPE ORA_ROWSCN ---------- ---------- ---------- 3 1 1247164
4) 會話2:更新id為3的type為1
19:26:05 SYS@ORA11GR2>update t_lock set type=1 whereora_rowscn=1246809 and id =3; 0 rows updated.
驗證:
19:29:37 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3; ID TYPE ORA_ROWSCN ---------- ---------- ---------- 3 1 1247164
(因為會話1的事務更改了id=3的值,而且事務已經提交,事務的ora_rowscn已經變為1247164,原來的ora_rowscn=1246809已經不存在,所以沒有可更改的行了)
1.4死鎖
1) 創建測試表
19:35:46 SYS@ORA11GR2>create table t_lock_1 (id number(2),name varchar2(15)); Table created. 19:35:57 SYS@ORA11GR2>create table t_lock_2 as select * from t_lock_1; Table created. 19:36:24 SYS@ORA11GR2>insert into t_lock_1 values(1,'liubei'); 1 row created. 19:37:11 SYS@ORA11GR2>insert into t_lock_2 values (1,'guanyu'); 1 row created. 19:37:38 SYS@ORA11GR2>commit; Commit complete. 19:37:43 SYS@ORA11GR2>select * from t_lock_1; ID NAME ---------- --------------- 1 liubei 19:38:01 SYS@ORA11GR2>select * from t_lock_2; ID NAME ---------- --------------- 1 guanyu
2) 會話1:更新表t_lock_1的id字段為1的name為“liuxuande”,不提交
19:39:55 SYS@ORA11GR2>update t_lock_1 set name='liuxuande' where id =1; 1 row updated.
3) 會話2:更新表t_lock_2的id字段為1的name為“關雲長”,不提交
19:39:47 SYS@ORA11GR2>update t_lock_2 set name='guanyunchang' where id = 1; 1 row updated.
4) 會話1:更新表t_lock_2的id字段為1的name為“guanyunchang”,此時掛起狀態
19:40:30 SYS@ORA11GR2>update t_lock_2 set name='guanyunchang' where id =1;
5) 會話2:更新表t_lock_1的id字段為1的name為“liuxuande”,此時掛起狀態
19:44:14 SYS@ORA11GR2>update t_lock_1 set name='liuxuande' where id =1;
6) 會話1:此時回到會話1,出現死鎖錯誤
19:40:30 SYS@ORA11GR2>update t_lock_2 set name='guanyunchang' where id =1; update t_lock_2 set name='guanyunchang' where id =1 * ERROR at line 1: ORA-00060: deadlock detected while waiting for resource
會話1處於死鎖狀態,而會話2處於掛起狀態。
樂觀鎖/悲觀鎖
悲觀鎖:顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
樂觀鎖: 顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。
悲觀鎖 和 樂觀鎖的區別:
兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生沖突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。
並發控制: 事務和鎖的存在都是為了更好的解決並發訪問造成的數據不一致性的的問題。
樂觀鎖和悲觀鎖都是為了解決並發控制問題, 樂觀鎖可以認為是一種在最后提交的時候檢測沖突的手段,而悲觀鎖則是一種避免沖突的手段。
樂觀鎖: 是應用系統層面和數據的業務邏輯層次上的(實際上並沒有加鎖,只不過大家一直這樣叫而已),利用程序處理並發, 它假定當某一個用戶去讀取某一個數據的時候,其他的用戶不會來訪問修改這個數據,但是在最后進行事務的提交的時候會進行版本的檢查,以判斷在該用戶的操作過程中,沒有其他用戶修改了這個數據。開銷比較小
樂觀鎖的實現大部分都是基於版本version控制實現的, 當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加1。當我們提交更新的時候,判斷當前版本信息與第一次取出來的版本值大小,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據,拒絕更新,讓用戶重新操作。
除此之外,還可以通過時間戳的方式,通過提前讀取,事后對比的方式實現。
寫到這里我突然想起了,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_READ, MySQL中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語句還是會加鎖的。
應用場景
ORM框架中悲觀鎖樂觀鎖的應用
一般悲觀鎖、樂觀鎖都需要都通過sql語句的設定、數據的設計結合代碼來實現,例如樂觀鎖中的版本號字段,單純面向數據庫操作,是需要自己來實現樂觀鎖的,簡言之,也就是版本號或時間戳字段的維護是程序自己維護的,自增、判斷大小確定是否更新都通過代碼判斷實現。數據庫進提供了樂觀、悲觀兩個思路進行並發控制。
對於常用java 持久化框架,對於數據庫的這一機制都有自己的實現,以Hibernate為例,總結一下ORM框架中悲觀鎖樂觀鎖的應用
1、Hibernate的悲觀鎖:
基於數據庫的鎖機制實現。如下查詢語句:
String hqlStr ="from TUser as user where user.name=Max"; Query query = session.createQuery(hqlStr); query.setLockMode("user",LockMode.UPGRADE); //加鎖 List userList = query.list();//執行查詢,獲取數據
觀察運行期Hibernate生成的SQL語句:
select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name='Erica' ) for update
這里Hibernate通過使用數據庫的for update子句實現了悲觀鎖機制。對返回的所有user記錄進行加鎖。
2、Hibernate的加鎖模式有:
LockMode.NONE : 無鎖機制。
LockMode.WRITE :Hibernate在寫操作(Insert和Update)時會自動獲取寫鎖。
LockMode.READ : Hibernate在讀取記錄的時候會自動獲取。
這三種鎖機制一般由Hibernate內部使用,如Hibernate為了保證Update過程中對象不會被外界修改,會在save方法實現中自動為目標對象加上WRITE鎖。
LockMode.UPGRADE :利用數據庫的for update子句加鎖。
LockMode. UPGRADE_NOWAIT :Oracle的特定實現,利用Oracle的for update nowait子句實現加鎖。
注意,只有在查詢開始之前(也就是Hiberate 生成SQL 之前)設定加鎖,才會真正通過數據庫的鎖機制進行加鎖處理,否則,數據已經通過不包含for update子句的Select SQL加載進來,所謂數據庫加鎖也就無從談起。
3、Hibernate的樂觀鎖
Hibernate 在其數據訪問引擎中內置了樂觀鎖實現。如果不用考慮外部系統對數據庫的更新操作,利用Hibernate提供的透明化樂觀鎖實現,將大大提升我們的生產力。Hibernate中可以通過class描述符的optimistic-lock屬性結合version描述符指定。具體實現方式如下:
現在,我們為之前示例中的TUser加上樂觀鎖機制。
實現一、 配置optimistic-lock屬性:
<hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version"> …… </class> </hibernate-mapping>
optimistic-lock屬性有如下可選取值:
none:無樂觀鎖
version:通過版本機制實現樂觀鎖
dirty:通過檢查發生變動過的屬性實現樂觀鎖
all:通過檢查所有屬性實現樂觀鎖
通過version實現的樂觀鎖機制是Hibernate官方推薦的樂觀鎖實現,同時也是Hibernate中,目前唯一在數據對象脫離Session發生修改的情況下依然有效的鎖機制。因此,一般情況下,我們都選擇version方式作為Hibernate樂觀鎖實現機制。
實現二、添加一個Version屬性描述符
<hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version"> <id name="id" column="id" type="java.lang.Integer"> <generator class="native"/> </id> <version column="version" name="version" type="java.lang.Integer"/> …… </class> </hibernate-mapping>
注意version 節點必須出現在ID 節點之后。這里聲明了一個version屬性,用於存放用戶的版本信息,保存在TUser表的version字段中。
測試:
此時如果我們嘗試編寫一段代碼,更新TUser表中記錄數據,如:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Max")); List userList = criteria.list(); TUser user =(TUser)userList.get(0); Transaction tx = session.beginTransaction(); user.setUserType(1); //更新UserType字段 tx.commit();
每次對TUser進行更新的時候,我們可以發現,數據庫中的version都在遞增。而如果我們嘗試在tx.commit 之前,啟動另外一個Session,對名為Max的用戶進行操作,下面模擬並發更新時的情況:
Session session= getSession();
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Max")); Session session2 = getSession(); Criteria criteria2 = session2.createCriteria(TUser.class); criteria2.add(Expression.eq("name","Max")); List userList = criteria.list(); List userList2 = criteria2.list();TUser user =(TUser)userList.get(0); TUser user2 =(TUser)userList2.get(0); Transaction tx = session.beginTransaction(); Transaction tx2 = session2.beginTransaction(); user2.setUserType(99); tx2.commit(); user.setUserType(1); tx.commit();
執行並發更新的代碼,在tx.commit()處拋出StaleObjectStateException異常,並指出版本檢查失敗,當前事務正在試圖提交一個過期數據。通過捕捉這個異常,我們就可以在樂觀鎖校驗失敗時進行相應處理。
這就是hibernate實現悲觀鎖和樂觀鎖的主要方式。