探索Mysql鎖機制(一)——樂觀鎖&悲觀鎖
Milestone
本文需要閱讀時間大約在1小時,請抽出完整的時間來閱讀,一目十行,真心沒用
後面會按照下圖,分批次對Mysql的鎖和大家一起分享

image.png
前言
資料庫的鎖機制是併發控制的重要內容,是對程式控制資料一致性的補充,更細粒度的保障資料的一致性,而使各種共享資源在被併發訪問變得有序所設計的一種規則。下面主要針對我們常見的InnoDB和Myisam進行解析。
注:下文提到的分庫分表、fail-fast理念如果有需要,可以給大家分享下,在我廠內部應用場景。
:hibiscus::hibiscus::hibiscus:聽著《嘴巴嘟嘟》,寫著文章,有種初唸的感覺。
花絮
小明是一家小作坊的屌絲程式設計師,工作3年,無房無車,有個女朋友叫"清風",一天一天又一天,過著無慾無求的屌絲生活,突然下雪的那天,聽說大廠某寶在招人:錢多事少妹紙穿的少、年終6月起步、有股票、上班不打卡、食堂超好、大神超多、可以直接對話18羅漢、老肖,甚至還可以撩馬爸爸!於是乎,小明血脈膨脹,氣血翻湧,熱淚盈眶,竟不能自已!閉關,苦練殺敵本領,2個月後,成功進入阿里,成為屌絲中的王者!於是乎,翻出祖傳寶典《程式設計師活下去的200個本事》之MYSQL篇。
有想來阿里的,可以聯絡我,內推你哦~
樂觀鎖&悲觀鎖
樂觀併發控制和悲觀併發控制是併發控制採用的主要方法。樂觀鎖和悲觀鎖不僅在關係資料庫裡應用,在Hibernate、Memcache等等也有相關概念。
1. 悲觀鎖
現在網際網路高併發的架構中,受到fail-fast思路的影響,悲觀鎖已經非常少見了。
悲觀鎖(Pessimistic Locking),悲觀鎖是指在資料處理過程,使資料處於鎖定狀態,一般使用資料庫的鎖機制實現。
1.1 資料表中的實現
在MySQL中使用悲觀鎖,必須關閉MySQL的自動提交,set autocommit=0,MySQL預設使用自動提交autocommit模式,也即你執行一個更新操作,MySQL會自動將結果提交。
set autocommit=0
舉個:chestnut:栗子:
假設商品表中有一個欄位quantity表示當前該商品的庫存量。假設有一件Dulex套套,其id為100,quantity=8個;如果不使用鎖,那麼操作方法
如下:
//step1: 查出商品剩餘量 select quantity from items where id=100; //step2: 如果剩餘量大於0,則根據商品資訊生成訂單 insert into orders(id,item_id) values(null,100); //step3: 修改商品的庫存 update Items set quantity=quantity-1 where id=100;
這樣子的寫法,在小作坊真的很正常,No Problems,但是在高併發環境下可能出現問題。
如下:

image.png
其實在①或者②環節,已經有人下單並且減完庫存了,這個時候仍然去執行step3,就造成了 超賣 。
但是使用悲觀鎖,就可以解決這個問題,在上面的場景中,商品資訊從查詢出來到修改,中間有一個生成訂單的過程,使用悲觀鎖的原理就是,當我們在查詢出items資訊後就把當前的資料鎖定,直到我們修改完畢後再解鎖。那麼在這個過程中,因為資料被鎖定了,就不會出現有第三者來對其進行修改了。而這樣做的前提是需要將要執行的SQL語句放在同一個事物中,否則達不到鎖定資料行的目的。
如下:
//step1: 查出商品狀態 select quantity from items where id=100 for update; //step2: 根據商品資訊生成訂單 insert into orders(id,item_id) values(null,100); //step3: 修改商品的庫存 update Items set quantity=quantity-2 where id=100;
select...for update是MySQL提供的實現悲觀鎖的方式。此時在items表中,id為100的那條資料就被我們鎖定了,其它的要執行select quantity from items where id=100 for update的事務必須等本次事務提交之後才能執行。這樣我們可以保證當前的資料不會被其它事務修改。
1.2 擴充套件思考
需要注意的是,當我執行select quantity from items where id=100 for update後。如果我是在第二個事務中執行select quantity from items where id=100(不帶for update)仍能正常查詢出資料,不會受第一個事務的影響。另外,MySQL還有個問題是select...for update語句執行中所有掃描過的行都會被鎖上,因此 在MySQL中用悲觀鎖務必須確定走了索引,而不是全表掃描,否則將會將整個資料表鎖住 。
悲觀鎖並不是適用於任何場景,它也存在一些不足,因為悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。如果加鎖的時間過長,其他使用者長時間無法訪問,影響了程式的併發訪問性,同時這樣對資料庫效能開銷影響也很大,特別是對長事務而言,這樣的開銷往往無法承受,這時就需要樂觀鎖。
在此和大家分享一下,在Oracle中,也存在select ... for update,和mysql一樣,但是Oracle還存在了select ... for update nowait,即發現被鎖後不等待,立刻報錯。
2. 樂觀鎖
樂觀鎖相對悲觀鎖而言,它認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回錯誤資訊,讓使用者決定如何去做。接下來我們看一下樂觀鎖在資料表和快取中的實現。
2.1 資料表中的實現
利用資料版本號( version )機制是樂觀鎖最常用的一種實現方式。一般通過為資料庫表增加一個數字型別的 “version” 欄位,當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此 version值+1 。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料,返回更新失敗。
放個被用爛了的圖

image.png
舉個栗子:chestnut::
//step1: 查詢出商品資訊 select (quantity,version) from items where id=100; //step2: 根據商品資訊生成訂單 insert into orders(id,item_id) values(null,100); //step3: 修改商品的庫存 update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};
既然可以用 version ,那還可以使用 時間戳 欄位,該方法同樣是在表中增加一個時間戳欄位,和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。
需要注意的是,如果你的資料表是讀寫分離的表,當master表中寫入的資料沒有及時同步到slave表中時會造成更新一直失敗的問題。此時,需要強制讀取master表中的資料(將select語句放在事物中)。
即:把select語句放在事務中,查詢的就是master主庫了!
2.2 樂觀鎖的鎖粒度
樂觀鎖在 我鳥系統 中廣泛用於狀態同步,我們經常會遇到併發對一條物流訂單修改狀態的場景,所以此時樂觀鎖就發揮了巨大作用。
分享一個精心挑選樂觀鎖,以此縮小鎖範圍的case
商品庫存扣減時,尤其是在秒殺、聚划算這種高併發的場景下,若採用version號作為樂觀鎖,則每次只有一個事務能更新成功,業務感知上就是大量操作失敗。
// 仍挑選以庫存數作為樂觀鎖 //step1: 查詢出商品資訊 select (inventory) from items where id=100; //step2: 根據商品資訊生成訂單 insert into orders(id,item_id) values(null,100); //step3: 修改商品的庫存 update items set inventory=inventory-1 where id=100 and inventory-1>0;
沒錯! 你參加過的天貓、淘寶秒殺、聚划算,跑的就是這條SQL ,通過挑選樂觀鎖,可以減小鎖力度,從而提升吞吐~
樂觀鎖需要靈活運用
現在網際網路高併發的架構中,受到fail-fast思路的影響,悲觀鎖已經非常少見了。
2.3 擴充套件訓練
在阿里很多系統中都能看到常用的features、params等欄位,這些欄位如果不進行版本控制,在併發場景下非常容易出現資訊覆蓋的問題。
比如:
執行緒 | 原始features | 目標features |
---|---|---|
T-A | a=1; | a=1;b=1; |
T-B | a=1; | a=1;c=1; |
我們期望最終更新的結果為:
a=1;b=1;c=1;
此時若SQL寫成了
update lg_order set features=#features# where order_id=#order_id#
那麼隨著T-A和T-B的先後順序不同,我們得到的結果有可能會是a=1;b=1;或a=1;c=1;
若此時採用樂觀鎖,利用全域性欄位version進行處理,則會發現與lg_order的其他欄位變更有非常高的衝突率,因為version欄位是全域性的
update lg_order set features=#features#, version=version+1 where order_id=#order_id# and version=#ori_version#
這種SQL會因為version的失敗而導致非常高的失敗率,當然咯,我其他欄位也在併發變更呀~
怎麼辦?
聰明的你會發現一般設計庫表的時,凡事擁有features類似欄位的,都會有一個features_cc與之成對出現,很多廠內年輕一輩的程式設計師很少注意到這個欄位,我們努力糾正過很久,現在應該好很多了。
features_cc的作用就是features的樂觀鎖版本的控制,這樣就規避了使用version與整個欄位衝突的尷尬。
update lg_order set features=#features#, features_cc= features_cc +1 where order_id=#order_id# and features_cc =#ori_ features_cc#
這裡需要注意的是,需要應用owner仔細review自己相關表的SQL,要求所有涉及到這個表features欄位的變更都必須加上features_cc= features_cc +1進行計算,否則會引起併發衝突, 平時要做好保護措施,不然很中意中標 。
在實際的環境中,這種高併發的場景中尤其多,大家思考一下是否自覺的加上了對features欄位的樂觀鎖保護。
不過需要提出的是,做這種欄位的精耕細作控制,是以提高維護成本作為代價的。
features、attribute這兩個欄位我們花費了很長時間才BU同學達成共識和review程式碼,要求用_cc來做版本控制。
若變更太頻繁,可以提出來單獨維護,做到冷熱資料分離。
今天就到這裡吧,有些讀者說我寫的東西偏簡單,寫技術類的文章也是循序漸進的過程,後面會逐漸加深技術難度和廣度,並且把在大廠中是遇到的坑分享給大家。
喜歡的同學可以獻花了~

image.png