Milestone
本文需要閱讀時間大約在1小時,請抽出完整的時間來閱讀,一目十行,真心沒用
后面會按照下圖,分批次對Mysql的鎖和大家一起分享
前言
數據庫的鎖機制是并發控制的重要內容,是對程序控制數據一致性的補充,更細粒度的保障數據的一致性,而使各種共享資源在被并發訪問變得有序所設計的一種規則。下面主要針對我們常見的InnoDB和Myisam進行解析。
注:下文提到的分庫分表、fail-fast理念如果有需要,可以給大家分享下,在我廠內部應用場景。
花絮
小明是一家小作坊的屌絲程序員,工作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
舉個??栗子:
假設商品表中有一個字段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,但是在高并發環境下可能出現問題。
如下:
其實在①或者②環節,已經有人下單并且減完庫存了,這個時候仍然去執行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值相等,則予以更新,否則認為是過期數據,返回更新失敗。
放個被用爛了的圖
舉個栗子??:
//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來做版本控制。
若變更太頻繁,可以提出來單獨維護,做到冷熱數據分離。
今天就到這里吧,有些讀者說我寫的東西偏簡單,寫技術類的文章也是循序漸進的過程,后面會逐漸加深技術難度和廣度,并且把在大廠中是遇到的坑分享給大家。
喜歡的同學可以獻花了~