MySql-兩階段加鎖協(xié)議

顧名思義,行鎖就是針對(duì)數(shù)據(jù)表中行記錄的鎖。這很好理解,比如事務(wù) A 更新了一行,而這時(shí)候事務(wù) B 也要更新同一行,則必須等事務(wù) A 的操作完成后才能進(jìn)行更新。

在實(shí)際情況下,SQL是千變?nèi)f化、條數(shù)不定的,數(shù)據(jù)庫很難在事務(wù)中判定什么是加鎖階段,什么是解鎖階段。于是引入了S2PL(Strict-2PL),即: 在事務(wù)中只有提交(commit)或者回滾(rollback)時(shí)才是解鎖階段,其余時(shí)間為加鎖階段。

舉個(gè)例子。在下面的操作序列中,事務(wù) B 的 update 語句執(zhí)行時(shí)會(huì)是什么現(xiàn)象呢?假設(shè)字段 id 是表 t 的主鍵。

實(shí)際上事務(wù) B 的 update 語句會(huì)被阻塞,直到事務(wù) A 執(zhí)行 commit 之后,事務(wù) B 才能繼續(xù)執(zhí)行。事務(wù) A 持有的兩個(gè)記錄的行鎖,都是在 commit 的時(shí)候才釋放的。在 InnoDB 事務(wù)中,行鎖是在需要的時(shí)候才加上的,但并不是不需要了就立刻釋放,而是要等到事務(wù)結(jié)束時(shí)才釋放。這個(gè)就是兩階段鎖協(xié)議。

兩階段加鎖對(duì)性能的影響,下面兩種不同的扣減庫存的方案:

方案1:begin;// 扣減庫存update t_inventory set count=count-5 where id= ${id} and count>=5;// 鎖住用戶賬戶表select * from t_user_account where user_id=123 for update;// 插入訂單記錄insert into t_trans; commit;

方案2:begin;// 鎖住用戶賬戶表select * from t_user_account where user_id=123 for update;// 插入訂單記錄insert into t_trans;// 扣減庫存update t_inventory set count=count-5 where id=${id} and count>=5;commit;

兩者方案的時(shí)序如下圖所示:

由于庫存往往是最重要的熱點(diǎn),是整個(gè)系統(tǒng)的瓶頸。那么如果采用第二種方案的話,tps應(yīng)該理論上能夠提升3rt/rt=3倍。這還僅僅是業(yè)務(wù)就只有三條SQL的情況下,多一條sql就多一次rt,就多一倍的時(shí)間。

根據(jù)兩階段鎖協(xié)議,不論你怎樣安排語句順序,所有的操作需要的行鎖都是在事務(wù)提交的時(shí)候才釋放的。所以,如果你把更新庫存 安排在最后,那么庫存這一行的鎖時(shí)間就最少。這就最大程度地減少了事務(wù)之間的鎖等待,提升了并發(fā)度。

值得注意的是:

在更新到數(shù)據(jù)庫的那個(gè)時(shí)間點(diǎn)才算鎖成功,提交到數(shù)據(jù)庫的時(shí)候才算解鎖成功,這兩個(gè)round_trip的前半段是不會(huì)計(jì)算在內(nèi)的:


從上面的例子中,可以看出,需要把最熱點(diǎn)的記錄,放到事務(wù)最后,這樣可以顯著的提高吞吐量。更進(jìn)一步:越熱點(diǎn)記錄離事務(wù)的終點(diǎn)越近(無論是commit還是rollback)

避免死鎖

這也是任何SQL加鎖不可避免的。上文提到了按照記錄Key的熱度在事務(wù)中倒序排列。 那么寫代碼的時(shí)候任何可能并發(fā)的SQL都必須按照這種順序來處理,不然會(huì)造成死鎖。如下圖所示:?

但是當(dāng)業(yè)務(wù)場景復(fù)雜,依然會(huì)有死鎖的可能,當(dāng)出現(xiàn)死鎖以后,有兩種策略:

一種策略是,直接進(jìn)入等待,直到超時(shí)。這個(gè)超時(shí)時(shí)間可以通過參數(shù) innodb_lock_wait_timeout 來設(shè)置。

另一種策略是,發(fā)起死鎖檢測,發(fā)現(xiàn)死鎖后,主動(dòng)回滾死鎖鏈條中的某一個(gè)事務(wù),讓其他事務(wù)得以繼續(xù)執(zhí)行。將參數(shù) innodb_deadlock_detect 設(shè)置為 on,表示開啟這個(gè)邏輯。

在 InnoDB 中,innodb_lock_wait_timeout 的默認(rèn)值是 50s,意味著如果采用第一個(gè)策略,當(dāng)出現(xiàn)死鎖以后,第一個(gè)被鎖住的線程要過 50s 才會(huì)超時(shí)退出,然后其他線程才有可能繼續(xù)執(zhí)行。對(duì)于在線服務(wù)來說,這個(gè)等待時(shí)間往往是無法接受的。但是,我們又不可能直接把這個(gè)時(shí)間設(shè)置成一個(gè)很小的值,比如 1s。這樣當(dāng)出現(xiàn)死鎖的時(shí)候,確實(shí)很快就可以解開,但如果不是死鎖,而是簡單的鎖等待呢?所以,超時(shí)時(shí)間設(shè)置太短的話,會(huì)出現(xiàn)很多誤傷。

所以,正常情況下我們還是要采用第二種策略,即:主動(dòng)死鎖檢測,而且 innodb_deadlock_detect 的默認(rèn)值本身就是 on。主動(dòng)死鎖檢測在發(fā)生死鎖的時(shí)候,是能夠快速發(fā)現(xiàn)并進(jìn)行處理的,但是它也是有額外負(fù)擔(dān)的。

你可以想象一下這個(gè)過程:每當(dāng)一個(gè)事務(wù)被鎖的時(shí)候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環(huán),最后判斷是否出現(xiàn)了循環(huán)等待,也就是死鎖。

當(dāng)有熱點(diǎn)數(shù)據(jù)被并發(fā)更新的話,每個(gè)新來的被堵住的線程,都要判斷會(huì)不會(huì)由于自己的加入導(dǎo)致了死鎖,雖然最終檢測的結(jié)果是沒有死鎖,但是這期間要消耗大量的 CPU 資源。因此,你就會(huì)看到 CPU 利用率很高,但是每秒?yún)s執(zhí)行不了幾個(gè)事務(wù)。

熱點(diǎn)行更新的解決策略: 降低并發(fā)度 :

1. 拆行,一行拆多行,減少鎖的沖突,以賬戶為例,可以考慮放在多條記錄上,比如 10 個(gè)記錄,賬戶總額等于這 10 個(gè)記錄的值的總和。這樣每次要給賬戶加金額的時(shí)候,隨機(jī)選其中一條記錄來加。這樣每次沖突概率變成原來的 1/10,可以減少鎖等待個(gè)數(shù),也就減少了死鎖檢測的 CPU 消耗。但其實(shí)這類方案需要根據(jù)業(yè)務(wù)邏輯做詳細(xì)設(shè)計(jì)。如果賬戶余額可能會(huì)減少,比如退款邏輯,那么這時(shí)候就需要考慮當(dāng)一部分行記錄變成 0 的時(shí)候,代碼要有特殊處理。

?2. Server 層限流,限制同一時(shí)間進(jìn)入更新的線程數(shù) ,比如在應(yīng)用網(wǎng)關(guān)層限流,或是使用sentinel之類的組件

3. 關(guān)閉死鎖監(jiān)測(關(guān)閉的弊端是可能超時(shí)較多)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容