悲觀鎖:
顧名思義,悲觀鎖,正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。 悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
使用場景舉例:我們以mysql的存儲引擎 InnoDB為例(如果不采用鎖機制)
商品表t_goods中有一個字段為status 為1表示該商品沒有下單,為2表示該商品已經被下單了,那么我們對某商品下單之前一定確保該商品未被下單,也就是說該商品的status=1
我們對一個商品goods_id = 1 的下單實例
1.先查出該商品的信息
select status from t_goods where goods_id = 1
2,然后根據商品信息生成訂單信息
insert into t_order(order_id,goods_id) values (null,1)
3.更新該商品的信息
update t_goods set status = 2 where goods_id = 1
上面的情況在高並發的情況會出現什么問題呢?
想一些在這種高並發情況下如果我們不采取任何的鎖機制,是不是會出現我們在處理這個事務(就是在下這個訂單的時候)別人是不是很可能已經下了這個商品的訂單,status被修改為2了,可是我們完全不知情,繼續下單,就可能會造成該商品被下單兩次,所以這種是極其不安全的
因此在上述情況中我們可以采用悲觀鎖
在上面的情況中,商品被查詢出來,有一個處理訂單的過程,我們可以使用悲觀鎖機制,當我們查出這個goods信息后,就把當前的數據鎖定起來,直到我們修改完畢再解鎖,所以在我們處理該goods的時候,就避免了第三者來修改
注意:使用悲觀鎖之前我們得關閉mysql的自動提交功能,因為mysql是默認開啟自動提交功能的
set autocommit = 0;
下面來簡單演示一下吧
1.開啟事務(三種方式都可以)
begin;
begin work;
start transaction;
2.查詢商品信息
select status from t_goods where goods_id = 1 for update;
3.根據查詢來的商品信息生成訂單
insert into t_order(order_id,goods_id) values (null,1);
4.修改該商品狀態為2
update t_goods set status = 2 where goods_id = 1;
5.提交
commit;
打開console1(明確指定主鍵,且查詢有數據,采用Row-Level Lock )
打開console2 (如果console1長時間不commit,這里會報錯)
與普通查詢不一樣的是,我們使用了select…for update的方式,這樣就通過數據庫實現了悲觀鎖。此時在t_goods表中,goods_id為1的 那條數據就被我們鎖定了,其它的事務必須等本次事務提交之后才能執行。這樣我們可以保證當前的數據不會被其它事務修改
注意:這里有個問題在是使用悲觀鎖的時候,如果第一個事務沒有commit我們是不能進行該相同數據的寫操作的,但是讀操作分兩種情況,在事務中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一筆數據時會等待其它事務結束后才執行,一般SELECT ... 則不受此影響。拿上面的實例來說,當我執行select status from t_goods where id=1 for update;后。我在另外的事務中如果再次執行select status from t_goods where id=1 for update;則第二個事務會一直等待第一個事務的提交,此時第二個查詢處於阻塞的狀態,但是如果我是在第二個事務中執行select status from t_goods where id=1;則能正常查詢出數據,不會受第一個事務的影響。
補充:MySQL select…for update的Row Lock與Table Lock
上面我們提到,使用select…for update會把數據給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認Row-Level Lock,所以只有「明確」地指定主鍵(id),MySQL 才會執行Row lock (只鎖住被選取的數據) ,否則MySQL 將會執行Table Lock (將整個數據表單給鎖住)。
如果明確有主鍵,沒有數據,則沒有lock
console1:沒數據查詢為空
console2:查詢結果為空,查詢無阻塞,說明console1沒有對數據執行鎖定
如果沒有主鍵,采用table lock;
console1:查詢正常
console2 :處於阻塞狀態,如果console1長時間不提交,他就會報錯,說明這里對全表進行了上鎖
如果沒有定義主鍵,采用table lock;
console1:正常查詢
console2:查詢被阻塞,說明表被console1鎖住了
以上就是關於數據庫主鍵對MySQL鎖級別的影響實例,需要注意的是,除了主鍵外,使用索引也會影響數據庫的鎖定級別
談到了MySQL悲觀鎖,但是悲觀鎖並不是適用於任何場景,它也有它存在的一些不足,因為悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。如果加鎖的時間過長,其他用戶長時間無法訪問,影響了程序的並發訪問性,同時這樣對數據庫性能開銷影響也很大,特別是對長事務而言,這樣的開銷往往無法承受。所以與悲觀鎖相對的,我們有了樂觀鎖,具體參見下面介紹
樂觀鎖介紹:
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。那么我們如何實現樂觀鎖呢,一般來說有以下2種方式:
1.使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。用下面的一張圖來說明:
如上圖所示,如果更新操作順序執行,則數據的版本(version)依次遞增,不會產生沖突。但是如果發生有不同的業務操作對同一版本的數據進行修改,那么,先提交的操作(圖中B)會把數據version更新為2,當A在B之后提交更新時發現數據的version已經被修改了,那么A的更新操作會失敗
2.樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。
使用舉例:以MySQL InnoDB為例
還是拿之前的實例來舉:商品goods表中有一個字段status,status為1代表商品未被下單,status為2代表商品已經被下單,那么我們對某個商品下單時必須確保該商品status為1。假設商品的id為1
輸出結果如下:
由上述可知我們同時查出同一個版本的數據,賦給不同的goods對象,然后先修改good1對象然后執行更新操作,執行成功。然后我們修改goods2,執行更新操作時提示操作失敗
這樣我們就簡單實現了一個樂觀鎖機制!