MySQL中的悲观锁和乐观锁
悲觀鎖(Pessimistic Lock)
顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處于鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
樂觀鎖(Optimistic Lock)
顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,像數據庫如果提供類似于write_condition機制的其實都是提供的樂觀鎖。
兩種鎖各有優缺點,不可認為一種好于另一種,像樂觀鎖適用于寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生沖突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。本質上,數據庫的樂觀鎖做法和悲觀鎖做法主要就是解決下面假設的場景,避免丟失更新問題:
一個比較清楚的場景
下面這個假設的實際場景可以比較清楚的幫助我們理解這個問題:假設當當網上用戶下單買了本書,這時數據庫中有條訂單號為001的訂單,其中有個status字段是’有效’,表示該訂單是有效的;
后臺管理人員查詢到這條001的訂單,并且看到狀態是有效的用戶發現下單的時候下錯了,于是撤銷訂單,假設運行這樣一條SQL: update order_table set status = ‘取消’ where order_id = 001;
后臺管理人員由于在b這步看到狀態有效的,這時,雖然用戶在c這步已經撤銷了訂單,可是管理人員并未刷新界面,看到的訂單狀態還是有效的,于是點擊”發貨”按鈕,將該訂單發到物流部門,同時運行類似如下SQL,將訂單狀態改成已發貨:update order_table set status = ‘已發貨’ where order_id = 001
觀點1:只有沖突非常嚴重的系統才需要悲觀鎖;“所有悲觀鎖的做法都適合于狀態被修改的概率比較高的情況,具體是否合適則需要根據實際情況判斷。”,表達的也是這個意思,不過說法不夠準確;的確,之所以用悲觀鎖就是因為兩個用戶更新同一條數據的概率高,也就是沖突比較嚴重的情況下,所以才用悲觀鎖。
觀點2:最后提交前作一次select for update檢查,然后再提交update也是一種樂觀鎖的做法,的確,這符合傳統樂觀鎖的做法,就是到最后再去檢查。但是wiki在解釋悲觀鎖的做法的時候,’It is not appropriate for use in web application development.’, 現在已經很少有悲觀鎖的做法了,所以我自己將這種二次檢查的做法也歸為悲觀鎖的變種,因為這在所有樂觀鎖里面,做法和悲觀鎖是最接近的,都是先select for update,然后update
在實際應用中我們在更新數據的時候,更嚴謹的做法是帶上更新前的“狀態”,如
update order_table set status = ‘取消’ where order_id = 001 and status = ‘待支付’ and …;
update order_table set status = ‘已發貨’ where order_id = 001 and status = ‘已支付’ and …;
然后在業務邏輯代碼里判斷更新的記錄數,為0說明數據庫已經被更新了,否則是正常的。
什么是悲觀鎖
在關系數據庫管理系統里,悲觀并發控制(又名“悲觀鎖”,Pessimistic Concurrency
Control,縮寫“PCC”)是一種并發控制的方法。它可以阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作讀某行數據應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖沖突的操作。
悲觀并發控制主要用于數據爭用激烈的環境,以及發生并發沖突時使用鎖保護數據的成本要低于回滾事務的成本的環境中。
簡而言之,悲觀鎖主要用于保護數據的完整性。當多個事務并發執行時,某個事務對數據應用了鎖,則其他事務只能等該事務執行完了,才能進行對該數據進行修改操作。
使用場景
在商品購買場景中,當有多個用戶對某個庫存有限的商品同時進行下單操作。若采用先查詢庫存,后減庫存的方式進行庫存數量的變更,將會導致超賣的產生。
商品超賣流程圖
若使用悲觀鎖,當B用戶獲取到某個商品的庫存數據時,用戶A則會阻塞,直到B用戶完成減庫存的整個事務時,A用戶才可以獲取到商品的庫存數據。則可以避免商品被超賣。
如何使用悲觀鎖
用法:SELECT … FOR UPDATE;
例如,
select * from tbl_user where id=1 for update;獲取鎖的前提:結果集中的數據沒有使用排他鎖或共享鎖時,才能獲取鎖,否則將會阻塞。
需要注意的是, FOR UPDATE 生效需要同時滿足兩個條件時才生效:
- 數據庫的引擎為 innoDB
- 操作位于事務塊中(BEGIN/COMMIT)
體驗悲觀鎖
Step 1 初始化表結構和數據
CREATE TABLE `tbl_user` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`status` int(11) DEFAULT NULL,`name` varchar(255) COLLATE utf8_bin DEFAULT NULL,PRIMARY KEY (`id`) );INSERT INTO `tbl_user` (`id`, `status`, `name`) VALUES(1,1,X'7469616E'),(2,1,X'63697479');Step 2
窗口1
窗口2
此時,我們在窗口2執行下面這條命令,嘗試獲取悲觀鎖:
執行完后,窗口2并沒有像窗口1一樣,立刻返回結果,而是發生了阻塞。
若超時間未獲取鎖,將會得到一個鎖超時錯誤提示。如下圖所示:
行鎖與表鎖
當執行 select … for update時,將會把數據鎖住,因此,我們需要注意一下鎖的級別。MySQL InnoDB 默認為行級鎖。當查詢語句指定了主鍵時,MySQL會執行「行級鎖」,否則MySQL會執行「表鎖」。
常見情況如下:
- 若明確指明主鍵,且結果集有數據,行鎖;
- 若明確指明主鍵,結果集無數據,則無鎖;
- 若無主鍵,且非主鍵字段無索引,則表鎖;
- 若使用主鍵但主鍵不明確,則使用表鎖;
小結: innoDB的行鎖是通過給索引上的索引項加鎖實現的,因此,只有通過索引檢索數據,才會采用行鎖,否則使用的是表鎖。
總結
悲觀鎖采用的是「先獲取鎖再訪問」的策略,來保障數據的安全。但是加鎖策略,依賴數據庫實現,會增加數據庫的負擔,且會增加死鎖的發生幾率。此外,對于不會發生變化的只讀數據,加鎖只會增加額外不必要的負擔。在實際的實踐中,對于并發很高的場景并不會使用悲觀鎖,因為當一個事務鎖住了數據,那么其他事務都會發生阻塞,會導致大量的事務發生積壓拖垮整個系統。
總結
以上是生活随笔為你收集整理的MySQL中的悲观锁和乐观锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Tomcat启动闪退的问题
- 下一篇: C++并发编程实战