Java併發 行級鎖/欄位鎖/表級鎖 樂觀鎖/悲觀鎖 共享鎖/排他鎖 死鎖[轉]
前言
鎖是防止在兩個事務操作同一個資料來源(表或行)時互動破壞資料的一種機制。
資料庫採用封鎖技術保證併發操作的可序列性。
以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) 建立測試表
[email protected]>create table t_lock as select rownum as id,0 as type from dual connect by rownum <=3; Table created. [email protected]>select * from t_lock; ID TYPE ---------- ---------- 1 0 2 0 3 0
2) 會話1:查詢type為0的最小id
[email protected]>set time on;
18:58:22 [email protected]>
18:58:23 [email protected]>select min(id) from t_lock where type=0;
MIN(ID)
----------
1
3) 會話2:查詢type為0的最小id
[email protected]>set time on
18:59:31 [email protected]>select min(id) from t_lock where type=0;
MIN(ID)
----------
1
4) 會話1:將ID為1的這條記錄的type置為1
19:00:53 [email protected]>update t_lock set type=1 where id=1;
1 row updated.
19:01:21 [email protected]>commit;
Commit complete.
19:01:37 [email protected]>select * from t_lock;
ID TYPE
---------- ----------
1 1
2 0
3 0
5) 會話2:將ID為1的這條記錄的type置為2
19:02:47 [email protected]>update t_lock set type=2 where id=1;
1 row updated.
19:03:11 [email protected]>commit;
Commit complete.
19:03:17 [email protected]>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 [email protected]>select * from t_lock where id=2 and type =0for update nowait;
ID TYPE
---------- ----------
2 0
2) 會話2:查詢id為2的記錄,此時查詢報錯
19:07:43 [email protected]>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 [email protected]>update t_lock set type=1 where id=2 and type=0;
1 row updated.
19:19:30 [email protected]>commit;
Commit complete.
19:19:39 [email protected]>select * from t_lock where id=2;
ID TYPE
---------- ----------
2 1
4) 會話2:查詢id為2的記錄,由於已經將id為2的type已經變為1,所以查不到資料了。
19:19:15 [email protected]>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 [email protected]>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 [email protected]>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 [email protected]>update t_lock set type=1 where ora_rowscn=1246809 and id = 3;
1 row updated.
19:25:29 [email protected]>commit;
Commit complete.
驗證:
19:28:22 [email protected]>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 [email protected]>update t_lock set type=1 whereora_rowscn=1246809 and id =3;
0 rows updated.
驗證:
19:29:37 [email protected]>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 [email protected]>create table t_lock_1 (id number(2),name varchar2(15));
Table created.
19:35:57 [email protected]>create table t_lock_2 as select * from t_lock_1;
Table created.
19:36:24 [email protected]>insert into t_lock_1 values(1,'liubei');
1 row created.
19:37:11 [email protected]>insert into t_lock_2 values (1,'guanyu');
1 row created.
19:37:38 [email protected]>commit;
Commit complete.
19:37:43 [email protected]>select * from t_lock_1;
ID NAME
---------- ---------------
1 liubei
19:38:01 [email protected]>select * from t_lock_2;
ID NAME
---------- ---------------
1 guanyu
2) 會話1:更新表t_lock_1的id欄位為1的name為“liuxuande”,不提交
19:39:55 [email protected]>update t_lock_1 set name='liuxuande' where id =1;
1 row updated.
3) 會話2:更新表t_lock_2的id欄位為1的name為“關雲長”,不提交
19:39:47 [email protected]>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 [email protected]>update t_lock_2 set name='guanyunchang' where id =1;
5) 會話2:更新表t_lock_1的id欄位為1的name為“liuxuande”,此時掛起狀態
19:44:14 [email protected]>update t_lock_1 set name='liuxuande' where id =1;
6) 會話1:此時回到會話1,出現死鎖錯誤
19:40:30 [email protected]>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實現悲觀鎖和樂觀鎖的主要方式。