來自:Hollis(微訊號:hollischuang)
作者:安靜的boy,Hollis做了簡單的修改及排版。
這是一篇介紹悲觀鎖和樂觀鎖的入門文章。旨在讓那些不瞭解悲觀鎖和樂觀鎖的小白們弄清楚什麼是悲觀鎖,什麼是樂觀鎖。不同於其他文章,本文會配上相應的圖解讓大家更容易理解。透過該文,你會學習到如下的知識。
1
鎖(Lock)
在介紹悲觀鎖和樂觀鎖之前,讓我們看一下什麼是鎖。
鎖,在我們生活中隨處可見,我們的門上有鎖,我們存錢的保險櫃上有鎖,是用來保護我們財產安全的。
程式中也有鎖,當多個執行緒修改共享變數時,我們可以給修改操作上鎖(syncronized)。
當多個使用者修改表中同一資料時,我們可以給該行資料上鎖(行鎖)。因此,鎖其實是在併發下控制多個操作的順序執行,以此來保證資料安全的變動。
並且,鎖是一種保證資料安全的機制和手段,而並不是特定於某項技術的。悲觀鎖和樂觀鎖亦是如此。本篇介紹的悲觀鎖和樂觀鎖是基於資料庫層面的。
2
悲觀鎖
悲觀鎖(Pessimistic Concurrency Control),第一眼看到它,相信每個人都會想到這是一個悲觀的鎖。沒錯,它就是一個悲觀的鎖。
那這個悲觀體現在什麼地方呢?悲觀是我們人類一種消極的情緒,對應到鎖的悲觀情緒,悲觀鎖認為被它保護的資料是極其不安全的,每時每刻都有可能變動,一個事務拿到悲觀鎖後(可以理解為一個使用者),其他任何事務都不能對該資料進行修改,只能等待鎖被釋放才可以執行。
資料庫中的行鎖,表鎖,讀鎖,寫鎖,以及syncronized實現的鎖均為悲觀鎖。
這裡再介紹一下什麼是資料庫的表鎖和行鎖,以免有的同學對後面悲觀鎖的實現看不明白。
我們經常使用的資料庫是mysql,mysql中最常用的引擎是Innodb,Innodb預設使用的是行鎖。而行鎖是基於索引的,因此要想加上行鎖,在加鎖時必須命中索引,否則將使用表鎖。
3
樂觀鎖
與悲觀相對應,樂觀是我們人類一種積極的情緒。樂觀鎖(Optimistic Concurrency Control)的“樂觀情緒”體現在,它認為資料的變動不會太頻繁。因此,它允許多個事務同時對資料進行變動。
但是,樂觀不代表不負責,那麼怎麼去負責多個事務順序對資料進行修改呢?
樂觀鎖通常是透過在表中增加一個版本(version)或時間戳(timestamp)來實現,其中,版本最為常用。
事務在從資料庫中取資料時,會將該資料的版本也取出來(v1),當事務對資料變動完畢想要將其更新到表中時,會將之前取出的版本v1與資料中最新的版本v2相對比,如果v1=v2,那麼說明在資料變動期間,沒有其他事務對資料進行修改,此時,就允許事務對錶中的資料進行修改,並且修改時version會加1,以此來表明資料已被變動。
如果,v1不等於v2,那麼說明資料變動期間,資料被其他事務改動了,此時不允許資料更新到表中,一般的處理辦法是通知使用者讓其重新操作。不同於悲觀鎖,樂觀鎖是人為控制的。
4
如何實現
經過上面的學習,我們知道悲觀鎖和樂觀鎖是用來控制併發下資料的順序變動問題的。那麼我們就模擬一個需要加鎖的場景,來看不加鎖會出什麼問題,並且怎麼利用悲觀鎖和樂觀鎖去解決。
場景:A和B使用者最近都想吃豬肉脯,於是他們開啟了購物網站,並且找到了同一家賣豬肉脯的>店鋪。下麵是這個店鋪的商品表goods結構和表中的資料。
id |
name |
num |
1 |
豬肉脯 |
1 |
2 |
牛肉乾 |
1 |
從表中可以看到豬肉脯目前的數量只有1個了。在不加鎖的情況下,如果A,B同時下單,就有可能導致超賣。
悲觀鎖解決
利用悲觀鎖的解決思路是,我們認為資料修改產生衝突的機率比較大,所以在更新之前,我們顯示的對要修改的記錄進行加鎖,直到自己修改完再釋放鎖。加鎖期間只有自己可以進行讀寫,其他事務只能讀不能寫。
A下單前先給豬肉脯這行資料(id=1)加上悲觀鎖(行鎖)。此時這行資料只能A來操作,也就是隻有A能買。B想買就必須一直等待。
當A買好後,B再想去買的時候會發現數量已經為0,那麼B看到後就會放棄購買。
那麼如何給豬肉脯也就是id=1這條資料加上悲觀鎖鎖呢?我們可以透過以下陳述句給id=1的這行資料加上悲觀鎖
select num from goods where id = 1 for update;
下麵是悲觀鎖的加鎖圖解
我們透過開啟mysql的兩個會話,也就是兩個命令列來演示。
1、事務A執行命令給id=1的資料上悲觀鎖準備更新資料
這裡之所以要以begin開始,是因為mysql是自提交的,所以要以begin開啟事務,否則所有修改將被mysql自動提交。
2、事務B也去給id=1的資料上悲觀鎖準備更新資料
我們可以看到此時事務B再一直等待A釋放鎖。如果A長期不釋放鎖,那麼最終事務B將會報錯,這有興趣的可以去嘗試一下。
3、接著我們讓事務A執行命令去修改資料,讓豬肉脯的數量減一,然後檢視修改後的資料,最後commit,結束事務。
我們可以看到,此時最後一個豬肉脯被A買走,只剩0個了。
4、當事務A執行完第3步後,我們看事務B中出現了什麼
我們看到由於事務A釋放了鎖,事務B就結束了等待,拿到了鎖,但是資料此時變成了0,那麼B看到後就知道被買走了,就會放棄購買。
透過悲觀鎖,我們解決了豬肉脯購買的問題。
樂觀鎖解決
下麵,我們利用樂觀鎖來解決該問題。上面樂觀鎖的介紹中,我們提到了,樂觀鎖是透過版本號version來實現的。所以,我們需要給goods表加上version欄位,表變動後的結構如下:
id |
name |
num |
version |
1 |
豬肉脯 |
1 |
0 |
1 |
牛肉乾 |
1 |
0 |
使用樂觀鎖的解決思路是,我們認為資料修改產生衝突的機率並不大,多個事務在修改資料的之前先查出版本號,在修改時把當前版本號作為修改條件,只會有一個事務可以修改成功,其他事務則會失敗。
A和B同時將豬肉脯(id=1下麵都說是id=1)的資料查出來,然後A先買,A將id=1和version=0作為條件進行資料更新,即將數量-1,並且將版本號+1。
此時版本號變為1。A此時就完成了商品的購買。最後B開始買,B也將id=1和version=0作為條件進行資料更新,但是更新完後,發現更新的資料行數為0,此時就說明已經有人改動過資料,此時就應該提示使用者重新檢視最新資料購買。
下麵是樂觀鎖的加鎖圖解
我們還是透過開啟mysql的兩個會話,也就是兩個命令列來演示。
1、事務A執行查詢命令,事務B執行查詢命令,因為兩者查詢的結果相同,所以下麵我只列出一個截圖。
此時A和B均獲取到相同的資料
2、事務A進行購買更新資料,然後再查詢更新後的資料。
我們可以看到事務A成功更新了資料和版本號。
事務B再進行購買更新資料,然後我們看影響行數和更新後的資料
可以看到最終修改行數為0,資料沒有改變。此時就需要我們告知使用者重新處理。
5
優缺點
下麵我們介紹下樂觀鎖和悲觀鎖的優缺點以便我們分析他們的應用場景,這裡我只分析最重要的優缺點,也是我們要記住的。
悲觀鎖
-
優點:悲觀鎖利用資料庫中的鎖機制來實現資料變化的順序執行,這是最有效的辦法
-
缺點:一個事務用悲觀鎖對資料加鎖之後,其他事務將不能對加鎖的資料進行除了查詢以外的所有操作,如果該事務執行時間很長,那麼其他事務將一直等待,那勢必影響我們系統的吞吐量。
樂觀鎖
-
優點:樂觀鎖不在資料庫上加鎖,任何事務都可以對資料進行操作,在更新時才進行校驗,這樣就避免了悲觀鎖造成的吞吐量下降的劣勢。
-
缺點:樂觀鎖因為是透過我們人為實現的,它僅僅適用於我們自己業務中,如果有外來事務插入,那麼就可能發生錯誤。
6
應用場景
悲觀鎖:因為悲觀鎖會影響系統吞吐的效能,所以適合應用在寫為居多的場景下。
樂觀鎖:因為樂觀鎖就是為了避免悲觀鎖的弊端出現的,所以適合應用在讀為居多的場景下。
參考資料
https://chenzhou123520.iteye.com/blog/1860954
https://baike.baidu.com/item/樂觀鎖/7146502
朋友會在“發現-看一看”看到你“在看”的內容