樂觀鎖和悲觀鎖

問題引入

當(dāng)程序中可能出現(xiàn)并發(fā)的情況時(shí),我們就需要通過一定的手段來保證在并發(fā)情況下數(shù)據(jù)的準(zhǔn)確性吨掌,通過這種手段保證了當(dāng)前用戶和其他用戶一起操作時(shí),所得到的結(jié)果和他單獨(dú)操作時(shí)的結(jié)果是一樣的。這種手段就叫做并發(fā)控制揪阿。并發(fā)控制的目的是保證一個(gè)用戶的工作不會對另一個(gè)用戶的工作產(chǎn)生不合理的影響雁仲。沒有做好并發(fā)控制仔夺,就可能導(dǎo)致臟讀、幻讀和不可重復(fù)讀等問題攒砖。

臟讀:事務(wù)A讀到了事務(wù)B未提交的數(shù)據(jù)缸兔。
不可重復(fù)讀:事務(wù)A第一次查詢得到一行記錄row1,事務(wù)B提交修改后吹艇,事務(wù)A第二次查詢得到row1惰蜜,但列內(nèi)容發(fā)生了變化。
幻讀:事務(wù)A第一次查詢得到一行記錄row1受神,事務(wù)B提交修改后抛猖,事務(wù)A第二次查詢得到兩行記錄row1和row2。

我們常說的并發(fā)控制,一般都和數(shù)據(jù)庫管理系統(tǒng)(DBMS)有關(guān)财著。在DBMS中的并發(fā)控制的任務(wù)联四,是確保在多個(gè)事務(wù)同時(shí)存取數(shù)據(jù)庫中同一數(shù)據(jù)時(shí),不破壞事務(wù)的隔離性和統(tǒng)一性以及數(shù)據(jù)庫的統(tǒng)一性撑教。

實(shí)現(xiàn)并發(fā)控制的手段

實(shí)現(xiàn)并發(fā)控制的主要手段大致可以分為樂觀并發(fā)控制和悲觀并發(fā)控制兩種朝墩。
在開始介紹之前要明確一下:無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念伟姐,可以認(rèn)為是一種思想收苏。其實(shí)不僅僅是關(guān)系型數(shù)據(jù)庫系統(tǒng)中有樂觀鎖和悲觀鎖的概念,像hibernate愤兵、tair鹿霸、memcache等都有類似的概念。所以恐似,不應(yīng)該拿樂觀鎖杜跷、悲觀鎖和其他的數(shù)據(jù)庫鎖等進(jìn)行對比。

悲觀鎖(Pessimistic Lock)

當(dāng)我們要對一個(gè)數(shù)據(jù)庫中的一條數(shù)據(jù)進(jìn)行修改的時(shí)候矫夷,為了避免同時(shí)被其他人修改葛闷,最好的辦法就是直接對該數(shù)據(jù)進(jìn)行加鎖以防止并發(fā)。這種借助數(shù)據(jù)庫鎖機(jī)制双藕,在修改數(shù)據(jù)之前先鎖定淑趾,再修改的方式被稱之為悲觀并發(fā)控制(又名“悲觀鎖”,Pessimistic Concurrency Control忧陪,縮寫“PCC”)扣泊。
悲觀鎖(Pessimistic Lock)

顧名思義,就是很悲觀嘶摊,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會修改延蟹,所以每次在拿數(shù)據(jù)的時(shí)候都會上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會block直到它拿到鎖叶堆。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制阱飘,比如行鎖,表鎖等虱颗,讀鎖沥匈,寫鎖等,都是在做操作之前先上鎖忘渔。它指的是對數(shù)據(jù)被外界(包括本系統(tǒng)當(dāng)前的其他事務(wù)高帖,以及來自外部系統(tǒng)的事務(wù)處理)修改持保守態(tài)度,因此畦粮,在整個(gè)數(shù)據(jù)處理過程中散址,將數(shù)據(jù)處于鎖定狀態(tài)乖阵。悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制(也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性爪飘,否則义起,即使在本系統(tǒng)中實(shí)現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù))师崎。

悲觀鎖主要是共享鎖或排他鎖

  • 共享鎖又稱為讀鎖,簡稱S鎖椅棺。顧名思義犁罩,共享鎖就是多個(gè)事務(wù)對于同一數(shù)據(jù)可以共享一把鎖,都能訪問到數(shù)據(jù)两疚,但是只能讀不能修改床估。
  • 排他鎖又稱為寫鎖,簡稱X鎖诱渤。顧名思義丐巫,排他鎖就是不能與其他鎖并存,如果一個(gè)事務(wù)獲取了一個(gè)數(shù)據(jù)行的排他鎖勺美,其他事務(wù)就不能再獲取該行的其他鎖递胧,包括共享鎖和排他鎖,但是獲取排他鎖的事務(wù)是可以對數(shù)據(jù)行讀取和修改赡茸。

悲觀并發(fā)控制實(shí)際上是“先取鎖再訪問”的保守策略缎脾,為數(shù)據(jù)處理的安全提供了保證。但是在效率方面占卧,處理加鎖的機(jī)制會讓數(shù)據(jù)庫產(chǎn)生額外的開銷遗菠,還有增加產(chǎn)生死鎖的機(jī)會。另外還會降低并行性华蜒,一個(gè)事務(wù)如果鎖定了某行數(shù)據(jù)辙纬,其他事務(wù)就必須等待該事務(wù)處理完才可以處理那行數(shù)據(jù)。

樂觀鎖( Optimistic Locking )

樂觀鎖是相對悲觀鎖而言的叭喜,樂觀鎖假設(shè)數(shù)據(jù)一般情況下不會造成沖突贺拣,所以在數(shù)據(jù)進(jìn)行提交更新的時(shí)候,才會正式對數(shù)據(jù)的沖突與否進(jìn)行檢測域滥,如果發(fā)現(xiàn)沖突了纵柿,則返回給用戶錯(cuò)誤的信息,讓用戶決定如何去做启绰。
顧名思義昂儒,就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會修改委可,所以不會上鎖渊跋,但是在更新的時(shí)候會判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù)腊嗡,可以使用版本號等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型拾酝,這樣可以提高吞吐量燕少,像數(shù)據(jù)庫如果提供類似于write_condition機(jī)制的其實(shí)都是提供的樂觀鎖。

兩種鎖各有優(yōu)缺點(diǎn)蒿囤,不可認(rèn)為一種好于另一種客们,像樂觀鎖適用于寫比較少的情況下,即沖突真的很少發(fā)生的時(shí)候材诽,這樣可以省去了鎖的開銷底挫,加大了系統(tǒng)的整個(gè)吞吐量。但如果經(jīng)常產(chǎn)生沖突脸侥,上層應(yīng)用會不斷的進(jìn)行retry建邓,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適睁枕。

實(shí)現(xiàn)方式

悲觀鎖實(shí)現(xiàn)方式

悲觀鎖的實(shí)現(xiàn)官边,往往依靠數(shù)據(jù)庫提供的鎖機(jī)制。在數(shù)據(jù)庫中外遇,悲觀鎖的流程如下:

  • 在對記錄進(jìn)行修改前注簿,先嘗試為該記錄加上排他鎖(exclusive locking)。
  • 如果加鎖失敗臀规,說明該記錄正在被修改滩援,那么當(dāng)前查詢可能要等待或者拋出異常。具體響應(yīng)方式由開發(fā)者根據(jù)實(shí)際需要決定塔嬉。
  • 如果成功加鎖玩徊,那么就可以對記錄做修改,事務(wù)完成后就會解鎖了谨究。
  • 期間如果有其他對該記錄做修改或加排他鎖的操作恩袱,都會等待我們解鎖或直接拋出異常。
  • 拿比較常用的MySql Innodb引擎舉例胶哲,來說明一下在SQL中如何使用悲觀鎖畔塔。

要使用悲觀鎖,我們必須關(guān)閉MySQL數(shù)據(jù)庫的自動提交屬性鸯屿。因?yàn)镸ySQL默認(rèn)使用autocommit模式澈吨,也就是說,當(dāng)我們執(zhí)行一個(gè)更新操作后寄摆,MySQL會立刻將結(jié)果進(jìn)行提交谅辣。(sql語句:set autocommit=0)

以淘寶下單過程中扣減庫存的需求說明一下悲觀鎖的使用:

// 1. 開啟事務(wù)
begin ;
// 2. 查詢商品的庫存信息
select quantity from items where id = 1 for update;
// 3. 修改商品的庫存
update items set quantity = 2 where id =1
// 4. 提交事務(wù)
commit;

以上,在對id = 1的記錄修改前婶恼,先通過for update的方式進(jìn)行加鎖桑阶,然后再進(jìn)行修改柏副。這就是比較典型的悲觀鎖策略。

如果以上修改庫存的代碼發(fā)生并發(fā)蚣录,同一時(shí)間只有一個(gè)線程可以開啟事務(wù)并獲得id=1的鎖割择,其它的事務(wù)必須等本次事務(wù)提交之后才能執(zhí)行。這樣我們可以保證當(dāng)前的數(shù)據(jù)不會被其它事務(wù)修改萎河。

上面我們提到荔泳,使用select…for update會把數(shù)據(jù)給鎖住,不過我們需要注意一些鎖的級別公壤,MySQL InnoDB默認(rèn)行級鎖换可。行級鎖都是基于索引的,如果一條SQL語句用不到索引是不會使用行級鎖的厦幅,會使用表級鎖把整張表鎖住,這點(diǎn)需要注意慨飘。

樂觀鎖的實(shí)現(xiàn)方式

使用樂觀鎖就不需要借助數(shù)據(jù)庫的鎖機(jī)制了确憨。

樂觀鎖的概念中其實(shí)已經(jīng)闡述了它的具體實(shí)現(xiàn)細(xì)節(jié)。主要就是兩個(gè)步驟:沖突檢測和數(shù)據(jù)更新瓤的。其實(shí)現(xiàn)方式有一種比較典型的就是CAS(Compare and Swap)休弃。

CAS是樂觀鎖技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí)圈膏,只有其中一個(gè)線程能更新變量的值塔猾,而其它線程都失敗,失敗的線程并不會被掛起稽坤,而是被告知這次競爭中失敗丈甸,并可以再次嘗試。

比如前面的扣減庫存問題尿褪,通過樂觀鎖可以實(shí)現(xiàn)如下:

// 1. 查詢商品的庫存信息,quantity=3
select quantity from items where id = 1;
// 2. 修改商品的庫存為2
update items set quantity = 2 where id =1 and  quantity = 3
commit;

以上睦擂,我們在更新之前,先查詢一下庫存表中當(dāng)前庫存數(shù)(quantity)杖玲,然后在做update的時(shí)候顿仇,以庫存數(shù)作為一個(gè)修改條件。當(dāng)我們提交更新的時(shí)候摆马,判斷數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前庫存數(shù)與第一次取出來的庫存數(shù)進(jìn)行比對臼闻,如果數(shù)據(jù)庫表當(dāng)前庫存數(shù)與第一次取出來的庫存數(shù)相等,則予以更新囤采,否則認(rèn)為是過期數(shù)據(jù)述呐。
比如說一個(gè)線程one從數(shù)據(jù)庫中取出庫存數(shù)3,這時(shí)候另一個(gè)線程two也從數(shù)據(jù)庫中取出庫存數(shù)3斑唬,并且two進(jìn)行了一些操作變成了2市埋,然后two又將庫存數(shù)變成3黎泣,這時(shí)候線程one進(jìn)行CAS操作發(fā)現(xiàn)數(shù)據(jù)庫中仍然是3,然后one操作成功缤谎。盡管線程one的CAS操作成功抒倚,但是不代表這個(gè)過程就是沒有問題的。
有一個(gè)比較好的辦法可以解決ABA問題坷澡,那就是通過一個(gè)單獨(dú)的可以順序遞增的version字段托呕。改為以下方式即可:

ABA

樂觀鎖每次在執(zhí)行數(shù)據(jù)的修改操作時(shí),都會帶上一個(gè)版本號频敛,一旦版本號和數(shù)據(jù)的版本號一致就可以執(zhí)行修改操作并對版本號執(zhí)行+1操作项郊,否則就執(zhí)行失敗。因?yàn)槊看尾僮鞯陌姹咎柖紩S之增加斟赚,所以不會出現(xiàn)ABA問題着降,因?yàn)榘姹咎栔粫黾硬粫p少。
除了version以外拗军,還可以使用時(shí)間戳任洞,因?yàn)闀r(shí)間戳天然具有順序遞增性。
以上SQL其實(shí)還是有一定的問題的发侵,就是一旦遇上高并發(fā)的時(shí)候交掏,就只有一個(gè)線程可以修改成功,那么就會存在大量的失敗刃鳄。

對于像淘寶這樣的電商網(wǎng)站盅弛,高并發(fā)是常有的事,總讓用戶感知到失敗顯然是不合理的叔锐。所以挪鹏,還是要想辦法減少樂觀鎖的粒度的。

// 修改商品庫存
update item
set quantity = quantity-1
where id = 1 and quantity -1 >0

以上SQL語句中掌腰,如果用戶下單數(shù)為1狰住,則通過quantity - 1 > 0的方式進(jìn)行樂觀鎖控制。

以上update語句齿梁,在執(zhí)行過程中催植,會在一次原子操作中自己查詢一遍quantity的值,并將其扣減掉1勺择。

高并發(fā)環(huán)境下鎖粒度把控是一門重要的學(xué)問创南,選擇一個(gè)好的鎖,在保證數(shù)據(jù)安全的情況下省核,可以大大提升吞吐率稿辙,進(jìn)而提升性能。

如何選擇

在樂觀鎖與悲觀鎖的選擇上面气忠,主要看下兩者的區(qū)別以及適用場景就可以了邻储。

  1. 樂觀鎖并未真正加鎖赋咽,效率高。一旦鎖的粒度掌握不好吨娜,更新失敗的概率就會比較高脓匿,容易發(fā)生業(yè)務(wù)失敗。

  2. 悲觀鎖依賴數(shù)據(jù)庫鎖宦赠,效率低陪毡。更新失敗的概率比較低。

隨著互聯(lián)網(wǎng)三高架構(gòu)(高并發(fā)勾扭、高性能毡琉、高可用)的提出,悲觀鎖已經(jīng)越來越少的被使用到生產(chǎn)環(huán)境中了妙色,尤其是并發(fā)量比較大的業(yè)務(wù)場景桅滋。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市身辨,隨后出現(xiàn)的幾起案子虱歪,更是在濱河造成了極大的恐慌,老刑警劉巖栅表,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異师枣,居然都是意外死亡怪瓶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門践美,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洗贰,“玉大人,你說我怎么就攤上這事陨倡×沧蹋” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵兴革,是天一觀的道長绎晃。 經(jīng)常有香客問我,道長杂曲,這世上最難降的妖魔是什么庶艾? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮擎勘,結(jié)果婚禮上咱揍,老公的妹妹穿的比我還像新娘。我一直安慰自己棚饵,他們只是感情好煤裙,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布掩完。 她就那樣靜靜地躺著,像睡著了一般硼砰。 火紅的嫁衣襯著肌膚如雪且蓬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天夺刑,我揣著相機(jī)與錄音缅疟,去河邊找鬼。 笑死遍愿,一個(gè)胖子當(dāng)著我的面吹牛存淫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沼填,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼桅咆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坞笙?” 一聲冷哼從身側(cè)響起岩饼,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎薛夜,沒想到半個(gè)月后籍茧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梯澜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年寞冯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晚伙。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吮龄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咆疗,到底是詐尸還是另有隱情漓帚,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布午磁,位于F島的核電站尝抖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏漓踢。R本人自食惡果不足惜牵署,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望喧半。 院中可真熱鬧奴迅,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至暇检,卻和暖如春产阱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背块仆。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工构蹬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悔据。 一個(gè)月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓庄敛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親科汗。 傳聞我的和親對象是個(gè)殘疾皇子藻烤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359