引言
為什么需要鎖(並發控制)
在並發的環境中,會存在多個用戶同時更新同一條數據,這時就會產生沖突。
沖突結果:
- 丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。
- 臟讀:當一個事務讀取其它完成一半事務的記錄時,就會發生臟讀取。
因此為了解決上述問題,引入了並發控制機制。
樂觀鎖(樂觀並發控制)和悲觀鎖(悲觀並發控制)是並發控制的主要手段, 其實不僅關系型數據庫中有樂觀鎖和悲觀鎖的概念,像redis,memcached等都有類似的概念。所以,不要把樂觀鎖和悲觀鎖狹隘的理解為DBMS中的概念,更不能把他們和數據庫中提供的鎖機制混為一談。其實,在DBMS中悲觀鎖正是利用了數據庫提供的鎖機制實現的。
因此我們針對不同的業務場景,應該選擇不同的並發控制方式。
一、悲觀鎖
悲觀鎖指的是外界對數據修改持保守態度(悲觀),因此,在整個數據處理過程中,將數據處於鎖定狀態。 悲觀鎖的實現,往往依靠數據庫提供的鎖機制 (也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
DBMS悲觀鎖是阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作 某行數據應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖沖突的操作。
悲觀並發控制主要用於數據爭用激烈的環境,以及發生並發沖突時使用鎖保護數據的成本要低於回滾事務的成本的環境中。
數據庫中,悲觀鎖的流程:
對任意記錄進行修改前,先嘗試為該記錄加上排他鎖(exclusive locking)。如果加鎖失敗,說明該記錄正在被修改,那么當前查詢可能要等待或者拋出異常。 具體響應方式由開發者根據實際需要決定。 如果成功加鎖,那么就可以對記錄做修改,事務完成后就會釋放鎖了。 其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。
使用悲觀鎖
要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性,因為MySQL默認使用auto commit模式,也就是說,當你執行一個更新操作后,MySQL會立刻將結果進行提交。set autocommit=0;
//0.開始事務
begin;/begin work;/start transaction; (三者選一就可以)
//1.查詢出商品信息
select status from t_goods where id=1 for update;
//2.根據商品信息生成訂單
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status為2
update t_goods set status=2;
//4.提交事務
commit;/commit work;
上面的查詢語句中,我們使用了select…for update的方式, 這樣就通過開啟排他鎖的方式實現了悲觀鎖。此時在t_goods表中,id為1的 那條數據就被我們鎖定了,其它的事務必須等本次事務提交之后才能執行。這樣我們可以保證當前的數據不會被其它事務修改。
注意點
我們需要注意一些鎖的級別,MySQL InnoDB默認行級鎖。 行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住。上面我們使用select…for update會把數據給鎖住(id上有主鍵索引),因此不會使用表級鎖。
如果查詢語句中有表關聯,只是用for update會鎖住所有表的相關記錄,建議使用for update of t.id。如果你不想讓你的事務等待其他事務的鎖,可以在for update 后加上 nowait,這樣當遇上沖突時數據庫會拋出異常。
優勢劣勢
悲觀並發控制實際上是"先取鎖再訪問"的保守策略,為數據處理的安全提供了保證。但是在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會;另外,在只讀型事務處理中由於不會產生沖突,也沒必要使用鎖,這樣做只能增加系統負載;還有會降低了並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數。
二、樂觀鎖
相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
相對於悲觀鎖,在對數據庫進行處理的時候,樂觀鎖並不會使用數據庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄數據版本。
實現數據版本有兩種方式:
第一種是使用版本號version,
第二種是使用時間戳timestamp(時間戳精度)。
數據版本,為數據增加的一個版本標識。當讀取數據時,將版本標識的值一同讀出,數據每更新一次,同時對版本標識進行更新。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的版本標識進行比對,如果數據庫表當前版本號與第一次取出來的版本標識值相等,則予以更新,否則認為是過期數據。
DBMS會假設多用戶並發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分數據。在提交數據更新之前,每個事務會先檢查在該事務讀取數據后,有沒有其他事務又修改了該數據(修改該數據的事務已提交,或未提交;事務未提交會帶來數據丟失)。如果其他事務有更新的話,正在提交的事務會進行回滾。
注:對於以上兩種方式,Hibernate自帶實現方式:在使用樂觀鎖的字段前加annotation: @Version, Hibernate在更新時自動校驗該字段。
使用版本號實現樂觀鎖
使用版本號時,可以在數據初始化時指定一個版本號,每次對數據的更新操作都對版本號執行+1操作。並判斷當前版本號是不是該數據的最新的版本號。
//1.查詢出商品信息
select status,version from t_goods where id=#{id}
//2.根據商品信息生成訂單
//3.修改商品status為2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
注意第二個事務執行update時,第一個事務已經提交了,所以第二個事務能夠讀取到第一個事務修改的version。
下面這種極端的情況:
我們知道MySQL數據庫引擎InnoDB,事務的隔離級別是Repeatable Read,因此是不會出現臟讀、不可重復讀。
在這種極端情況下,第二個事務的update由於不能讀取第一個事務未提交的數據(第一個事務已經對這一條數據加了排他鎖,第二個事務需要等待獲取鎖),第二個事務獲取了排他鎖后,會發現version已經發生了改變從而提交失敗。
優勢劣勢
樂觀並發控制相信事務之間的數據競爭(datarace)的概率是比較小的, 因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。
樂觀鎖雖然沒有依賴數據庫提供的鎖機制,也可以保證數據一致性。