1. 程式人生 > >徹底搞定歡樂鎖與悲觀鎖

徹底搞定歡樂鎖與悲觀鎖

本文並未全部原創,感覺網路上的知識比較混亂,故自己整理了一下。

樂觀併發控制(樂觀鎖)和悲觀併發控制(悲觀鎖)是併發控制採用的技術手段,是由人們定義出來的概念。可以認為是一種思想。

針對不同的業務情景,應該選用不同的併發控制方式。所以,不要把樂觀鎖和悲觀鎖狹義的理解為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