徹底搞定歡樂鎖與悲觀鎖
本文並未全部原創,感覺網路上的知識比較混亂,故自己整理了一下。
樂觀併發控制(樂觀鎖)和悲觀併發控制(悲觀鎖)是併發控制採用的技術手段,是由人們定義出來的概念。可以認為是一種思想。
針對不同的業務情景,應該選用不同的併發控制方式。所以,不要把樂觀鎖和悲觀鎖狹義的理解為DBMS(資料庫管理)中的概念,更不要與資料庫中提供的鎖機制(行鎖、表鎖、共享鎖、排他鎖)混為一談。
首先了解下資料庫鎖的概念,才能更好的理解樂觀鎖與悲觀鎖。
資料庫鎖的概念
共享鎖(S鎖)
如果事務T對資料A加上共享鎖後,則其他事務只能對A再加共享鎖,不能加排他鎖,獲取共享鎖的事務只能讀取資料,不能修改資料。
排他鎖(X鎖)
如果事務T對資料A加上排他鎖後,則其他事務不能再對A加任何型別的封鎖。獲取排他鎖的事務既能讀資料,又能修改資料。
例1:
T1: select * from table (執行1小時之久);
T2: update table set column1 =’hello’;
過程:
T1: 執行,加共享鎖
T2: 執行
只有T1執行完畢釋放鎖之後T2才能執行
T2之所以需要等,是因為T2在執行update錢,試圖對table表加了一個排他鎖,而資料庫規定同一資源上不能同時存在共享鎖和排他鎖。所以T2必須等待T1釋放了共享鎖,才能加上排他鎖,然後執行Update語句
例2:
T1: select * from table1
T2: select * from table1
過程:
在這裡,T2不用等待T1執行完畢,而是馬上執行。
分析:
兩個共享鎖都是同時存在同一個資源上,這被稱之為共享鎖與共享鎖相容。這意味著共享鎖不組織其它session同時讀取資源。但組織其它session update;
例3:
T1: select * from table1
T2: select * from table1
T3: update table set column1 =’hello’;
分析:
這裡T2不需要等待T1完成之後執行,而T3需要等待T1、T2都完成之後才能執行,因為T3必須等T1、T2釋放共享鎖才能進行加排他鎖執行update。
死鎖的產生
T1:begin tran
Select * from table (holdlock)--共享鎖,直到事務結束後才釋放
Update table set column1=’hello’
T2:begin tran
Select * from table (holdlock)
Update table set column1=’hello’
分析:
假設T1,T2同時到達select,T1對table加共享鎖,T2也加共享鎖,當T1的select執行完,準備執行update時候,由於T2的共享鎖還沒有釋放,必須等table上的其他共享鎖釋放之後才能進行update,但因為holdlock這樣的共享鎖只有等事務結束後才能釋放,所以T2的共享鎖不釋放,而導致T1一直在等。這樣,死鎖就產生了。
例5:
T1:
Begin tran
Update table set column1 =’hello’ where id = 10
T2:
Begin tran
Update table set column1 =’hello’ where id = 20
分析:
這種情況也會產生死鎖,但是既要看情況。如果id是主鍵上面有索引,那麼T1一下就找到id=10的這條記錄,人後對該條記錄加排他鎖。T2同樣,也是一下子通過索引定位到記錄id=20的這條記錄,對該條記錄加排他鎖,那麼T1和T2之間個更新各的,互不影響。
如果id是普通的一列,沒有索引,那麼當T1對id=10這條加排他鎖之後,T2為了找到id=20,需要對全表掃面,那麼久會對預先對錶加上了共享鎖或者更新鎖或者排他鎖(依賴於資料庫執行策略和方式,比如第一次執行和第二次執行,資料庫的執行策略就不通)。但是因為T1已經為一挑記錄加了排他鎖,導致T2的全表掃描進行不下去了,就導致T2一直等待。
死鎖如何解決呢?
例6:
T1:begin tran
Select * from table (xlock)--直接對錶加排他鎖
Update table set column1=’hello’
T2:begin tran
Select * from table (xlock)
Update table set column1=’world’
分析:
因為排他鎖既可以查詢也可以更新,所以T1執行後,T2開始執行,發現table表已經被T1加上了排他鎖,就需要等待T1的事務完成之後才執行。排除了死鎖發生。
但是第三個user過來想查詢語句時,也因為排他鎖的存在,不得不等待,第四個、第五個user都會因此等待,在大併發的情況下,讓大家等待顯得效能就不太友好了,所以這裡引入了更新鎖。
更新鎖(Update lock)
更新鎖為了防止常見形式的死鎖。更新鎖的意思是:“我現在只想讀,別人也可以讀,但我將來可能有更新操作,我已經獲取了從共享鎖(用來讀)到排他鎖的資格”。一個事務只能獲取一個更新鎖。
例7:
T1:begin tran
Select * from table (updlock)--直接對錶加更新鎖
Update table set column1=’hello’
T2:begin tran
Select * from table (updlock)
Update table set column1=’world’
分析:
T1執行select,加更新鎖。
T2執行,準備加更新鎖,但我發現已經有所在,只好等。
當後來user3、4......需要查詢table表中的資料時,並不會因為T1的select在執行就被阻塞,正常查詢。
例8:
T1: select * from table(updlock) (加更新鎖)
T2: select * from table(updlock) (等待,直到T1釋放更新鎖,因為同一時間不能在同一資源上有兩個更新鎖)
T3: select * from table (加共享鎖,但不用等updlock釋放,就可以讀)
分析:
這個例子是說明:共享鎖和更新鎖可以同時在同一個資源上。這被稱為共享鎖和更新鎖是相容的。
例9:
T1:
begin
select * from table(updlock) (加更新鎖)
update table set column1='hello' (重點:這裡T1做update時,不需要等T2釋放什麼,而是直接把更新鎖升級為排他鎖,然後執行update)
T2:
begin
select * from table (T1加的更新鎖不影響T2讀取)
update table set column1='world' (T2的update需要等T1的update做完才能執行)
分析:
第一種情況:T1先達,T2緊接到達;在這種情況中,T1先對錶加更新鎖,T2對錶加共享鎖,假設T2的select先執行完,準備執行update,
發現已有更新鎖存在,T2等。T1執行這時才執行完select,準備執行update,更新鎖升級為排他鎖,然後執行update,執行完成,事務
結束,釋放鎖,T2才輪到執行update。
第二種情況:T2先達,T1緊接達;在這種情況,T2先對錶加共享鎖,T1達後,T1對錶加更新鎖,假設T2 select先結束,準備
update,發現已有更新鎖,則等待,後面步驟就跟第一種情況一樣了。
排他鎖與更新鎖是不相容的,它們不能同時加在同一子資源上。
意向鎖
比如一個屋子裡,門口有一個標識,標識說明了屋子裡有人被鎖住了。另一個人想知道屋子裡有沒有人被鎖,不用進屋裡來看,直接看門口標識就行了。
當一個表中的某一行被加上排他鎖後,該表就不能被加表鎖,資料庫如何判斷該表能不能加表鎖?一種方式是逐條判斷,是否加上排他鎖,另一種方式是直接檢查表本身時候有意向鎖。
例12:
T1: begin tran
select * from table (xlock) where id=10 --意思是對id=10這一行強加排他鎖
T2: begin tran
select * from table (tablock) --意思是要加表級鎖
假設T1先執行,當T2執行時,欲加表鎖,為了判斷時候可以加鎖,資料庫系統要逐條判斷是否有排他鎖,如果發現其中有排他鎖了,就不允許加表鎖了。
實際上資料庫不是這樣操作的,當T1執行時候,系統對錶id=10這一樣加了排他鎖,同時還偷偷的為整個表加了意向排他鎖,當T2執行鎖表時候,看到排他鎖存在就一直等待。不需要逐條檢查資源了。
例13:
T1: begin tran
update table set column1='hello' where id=1
T2: begin tran
update table set column1='world' where id=1
這個例子和上面的例子實際效果相同,T1執行,系統對table同時對行家排他鎖、對頁加意向排他鎖、對錶加意向排他鎖。
計劃鎖(Schema Locks)
例14:
Alter table ...(加schema locks)
DDL語句都會加Sch-M鎖
DDl:資料定義語言的縮寫,就是對資料庫內部的物件進行建立、刪除、修改等操作的語言。它和DML語句的最大區別是DML只是對錶內部資料操作,而不涉及表的定義、結構的修改,更不會涉及其他物件。DDL語句更多地由資料庫管理員(DBA)使用。
該鎖不允許任何其它session連線該表。連都連不了這個表了,當然更不用說想對該表執行什麼sql語句了。
例15:
用jdbc向資料庫傳送了一條新的sql語句,資料庫要先對之進行編譯,在編譯期間,也會加鎖,稱之為:Schema stability (Sch-S) locks
select * from tableA
編譯這條語句過程中,其它session可以對錶tableA做任何操作(update,delete,加排他鎖等等),但不能做DDL(比如alter table)操作。
何時加鎖
可以通過hint手工強行指定,但大多數由資料庫系統自動決定。
例16:
T1: begin tran
update table set column1='hello' where id=1
T2: select * from table where id=1 --為指定隔離級別,則使用系統預設隔離級別,它不允許髒讀
如果事物級別不設為髒讀,則:
1) T1執行,資料庫自動加排他鎖
2) T2執行,資料庫發現事物隔離級別不允許髒讀,便準備為此次select過程加共享鎖,但發現加不上,因為已經有排他鎖了,所以就等啊等。直到T1執行完,釋放了排他鎖,T2才加上了共享鎖,然後開始讀....
鎖的粒度
鎖的粒度就是指鎖的生效範圍,如:行鎖、頁鎖、整表鎖。鎖的粒度同樣可以有資料庫管理,也可以通過hint來管理。
例17:
T1: select * from table (paglock)
T2: update table set column1='hello' where id>10
T1執行後,對第一頁加鎖,讀完第一頁之後釋放鎖在對第二頁加鎖,假設10記錄簽好是第一頁最後一條,那麼,T1執行第一頁查詢時,並不會阻塞T2更新。
例18:
T1: select * from table (rowlock)
T2: update table set column1='hello' where id=10
T1執行時,對每行加共享鎖,讀取,然後釋放,再對下一行加鎖;T2執行時,會對id=10的那一行試圖加鎖,只要該行沒有被T1加上行鎖,T2就可以順利執行update操作。
例19:
T1: select * from table (tablock)
T2: update table set column1='hello' where id = 10
T1執行,對整個表加共享鎖. T1必須完全查詢完,T2才可以允許加鎖,並開始更新。
鎖與事務隔離級別的優先順序
手工指定的鎖優先。
例20:
T1: GO
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
GO
BEGIN TRANSACTION
SELECT * FROM table (NOLOCK)
GO
T2: update table set column1='hello' where id=10
T1是事物隔離級別為最高階,序列鎖,資料庫系統本應對後面的select語句自動加表級鎖,但因為手工指定了NOLOCK,所以該select語句不會加任何鎖,所以T2也就不會有任何阻塞。
鎖的超時等待
例26:
SET LOCK_TIMEOUT 4000 用來設定鎖等待時間,單位是毫秒,4000意味著等待
4秒可以用select @@LOCK_TIMEOUT檢視當前session的鎖超時設定。-1 意味著
永遠等待。
T1: begin tran
udpate table set column1='hello' where id = 10
T2: set lock_timeout 4000
select * from table wehre id = 10
T2執行時,會等待T1釋放排他鎖,等了4分鐘,如果T1還沒有釋放,T2就會丟擲異常:Lock request time out period exceeded.
悲觀鎖
在整個事務過程中,將資料處於鎖定狀態。只有當這個事務把鎖釋放,其他事務才能執行與該鎖衝突的操作。
悲觀鎖的流程:
在對於任意記錄進行修改前,都嘗試為該條記錄加上排他鎖,如果加鎖失敗,說明該記錄正在被修改,需要等待或者丟擲異常,具體有由開發者根據實際需要決定。
如果成功,那麼就可以對記錄修改,事務完畢後就會解鎖,期間如果有其他對該記錄的修改或加排他鎖的操作,都會等待解鎖或丟擲異常。
對於Mysql innoDB中使用悲觀鎖
使用悲觀鎖,必須關閉mysql資料庫的自動提交屬性,因為MySQL預設使用autocommit模式,也就是說,當執行一個更新操作後,Mysql會立即將結果提交。set autocommit 0;
使用場景
商品goods表中有一個欄位status,status為1代表商品沒有被下單,status為2代表商品已經被下單,如果我們對某個商品下單時,必須確保商品status為1才可以下單。假設商品id為1
如果不採用鎖,那麼操作方法如下:
--1.查出商品資訊
Select status from t_goods where id = 1;
--2根據商品資訊生成訂單
Inset into t_orders(id,goods_id) values(null,1);
--3.修改商品status為2
Update t_goods set status =2
上面的這種場景在高併發訪問的情況下很有可能出現問題。
前面說,只有goods的status為1才能對該商品下單。在第一步操作中,查出商品status為1,但是當我們執行第三步update操作的時候,有可能出現其他人先一步把商品status修改為2了,但是我們並不知道資料已經被修改了,這樣就導致同一個商品被下單2次,導致資料不一致,這種方式是不安全的。
使用悲觀鎖來實現
使用悲觀鎖的原理就是當我們查詢出goods資訊的時候就把當前資料加鎖,直到我們修改完畢後再釋放鎖,那麼在這個過程中,因為goods被鎖定了,就不會出現第三者對其進行修改。
首先,設定autocommit = 0;
--開啟事務
Begin;/begin work;/start transaction;(三者選一即可)
--查出商品資訊;
Select status from t_goods where id = 1 for update;
--根據商品資訊生成訂單
Insert into t_orders(id,goods_id)values(null,1);
--修改商品status為2
Update t_goods set status = 2;
--提交事務
Commit;/commit work;
注意:上面的begin/commit為事務的開始和結束,因為在之前我們關閉了mysql的autocommit,所以需要手動控制事務提交。
與普通查詢不同的是,我們使用了select ...for update的方式,這樣就通過資料庫實現了悲觀鎖。這時在t_goods表中。Id =1的那條記錄就被鎖定,其他事務必須等本次事務提交之後才能執行。這樣我們就可以保證之前的資料不會被其他事務修改。
在事務中,只用SELECT ... FOR UPDATE(加排他鎖)或SELECT ... LOCK IN SHARE MODE(加共享鎖)操作同一組資料時會等待其他事務結束後才執行。對於一般的select...不收影響。比如:select status form goods where id = 1 for update;後,在另一個事務中如果再次執行select status from goods where id =1 for update 則第二個事務會一直等待第一個事務提交。此時第二個查詢處於阻塞的狀態,但如果在第二個事務中執行的是select status from goods where id = 1,則能正常查詢資料,不受第一個事務的影響。
補充:mysql的select for update的row lock 與 table lock
上面說,使用select ... for update 會把資料給鎖住,不過我們需要注意一些鎖的級別,MySql innoDB 預設 Row-Level lock,所以只有明確的指定主鍵/索引,Mysql才會執行 Row lock(鎖住被選取的資料),否則Mysql 會執行 Table Lock (將整個表給鎖住)。
優點與不足:悲觀鎖實際上是“先加鎖在訪問”的保守策略,為資料處理的安全提供了保證,但是在效率方面,處理加鎖機制會讓資料庫產生額外的開銷,並且增加了死鎖的可能性。另外,在只讀型事務中沒必要使用鎖,這樣只能增加系統的負載,降低了併發性。
樂觀鎖
相對於悲觀鎖而言,樂觀鎖假設認為一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突進行檢測,如果發現衝突了,返回錯誤資訊,讓使用者去處理。實現樂觀鎖有一下兩種方式:
1. 使用資料庫版本(version)記錄的機制來實現,何為資料庫版本?及為增加一個版本標識,一般是通過資料庫表增加一個version欄位來實現,當讀取資料時,將version一併帶出,資料每更新一次就對version+1,當我們提交更新的時候,判斷version是否是與取出來的version值一致,一致則予以更新,不一致則認為過期資料。
2. 使用時間戳來標誌版本,跟version類似,也是在更新的時候,判斷時間戳是否與讀出來的時間戳是否一致,一致則予以更新,否則版本衝突
使用舉例:
--查詢出商品資訊
Select status version from goods where id = #id;
--根據商品資訊生成訂單
--修改商品status為2
Update goods set sttatus = 2 version = version +1 where id = #id and version = #version;
優點與不足
樂觀鎖假設認為不會造成衝突,只有在提交的時候才去鎖定,所以不會產生任何鎖和死鎖。但是如果直接這麼做,還是有可能遇到不同預期的結果,例如aba問題(aba:如果另一個執行緒修改V值假設原來是A,先修改成B,再修改回成A。當前執行緒的CAS操作無法分辨當前V值是否發生過變化)。
---------------------
作者:趙慶春
原文:https://blog.csdn.net/wangaiheng/article/details/79789699