高併發場景下的數據庫事務調優

作者丨故里

來源丨故里學 Java

數據庫事務是訪問可能操作各種數據項的一個數據庫操作序列,這些操作要麼全部成功,要麼全部失敗。提起事務,大家都知道 ACID 屬性,這些特性在前邊的文章裏都有詳細的講解,感興趣的可以通過歷史文章查看。在 Java 中有併發編程,可以多線程併發執行,併發可以提高程序執行的效率,也會帶來線程安全的。數據庫事務和多線程一樣,爲了提高數據庫處理事務的吞吐量,數據庫也支持併發事務,在併發處理數據的過程中,也存在着安全問題。

我們本文將從併發事務可能引發的問題、解決併發問題、MySQL 的鎖機制、鎖的實現等方面逐漸深入,探討高併發場景下的事務調優問題。

併發事務可能引發的問題

1. 數據丟失

2. 髒讀、

3. 幻讀

4. 不可重複讀

事務隔離解決的併發問題

數據丟失可以基於數據庫中的悲觀鎖來避免發生,即在查詢時通過在事務中使用 select xx for update 語句來實現一個排他鎖,保證在該事務結束之前其他事務無法更新該數據。我們也可以基於樂觀鎖來避免,即將某一字段作爲版本號,如果更新時的版本號跟之前的版本一致,則更新,否則更新失敗。剩下 3 個問題,其實是數據庫讀一致性造成的,需要數據庫提供一定的事務隔離機制來解決。

MySQL 的鎖機制

InnoDB 實現了兩種類型的鎖機制:共享鎖(S)和排他鎖(X)。共享鎖允許一個事務讀數據,不允許修改數據,如果其他事務要再對該行加鎖,只能加共享鎖;排他鎖是修改數據時加的鎖,可以讀取和修改數據,一旦一個事務對該行數據加鎖,其他事務將不能再對該數據加任務鎖。

不同的鎖機制會產生不同的事務隔離級別,不同的隔離級別分別可以解決併發事務產生的問題,如讀未提交、讀已提交、可重複讀、可序列化等。(1 號發的《MySQL 的事務隔離級別和長事務,看這一篇就夠了》一文中有介紹過)

InnoDB 中的讀已提交和可重複讀隔離事務是基於多版本併發控制(MVCC)實現高性能事務。一旦數據被加上排他鎖,其他的事務將無法加入共享鎖,且處於阻塞等待狀態,如果一張表有大量的請求,這樣的性能將是無法支持的。

MVCC 對普通的 Select 不加鎖,如果讀取的數據正在執行 delete 或者 update 操作,這時讀取操作不會等待排他鎖的釋放,而是直接利用 MVCC 讀取該行的數據快照。MVCC 避免了對數據重複加鎖的過程,大大提高了毒草在的性能。(數據快照是指在該行的之前版本的數據,而數據快照的版本是基於 undo 實現的,undo 是用來做事務回滾的,記錄了回滾的不同版本的行記錄)

鎖的具體實現算法

InnoDB 既實現了行鎖,也實現了表鎖,行鎖是通過索引實現的,如果不通過索引條件檢索數據,那麼 InnoDB 將表中所有的記錄進行加鎖,其實就是升級爲表鎖。

行鎖的具體實現算法有三種:record lock、gap lock 和 next-key lock。record lock 是專門對索引項加鎖;gap lock 是對索引項之間的間隙加鎖,next-key lock 則是前面兩種的組合,對索引項及其之間的間隙加鎖。

只在可重複讀或以上隔離級別下的特定操作纔會取得 gap lock 或 next-key lock,在 Select 、Update 和 Delete 時,除了基於唯一索引的查詢之外,其他索引查詢時都會獲取 gap lock 或 next-key lock,即鎖住其掃描的範圍。

優化高併發事務

上邊的講解,都是爲了對事務、鎖和隔離級別更加深入瞭解,下邊將聊聊高併發場景下的事務是如何調優的。

  1. 結合業務場景,使用低級別事務隔離

在高併發業務中,爲了保證業務數據的一致性,操作數據庫時往往會使用不同級別的事務隔離,隔離等級越高,併發性能就越低。

那在實際的業務中,我們要如何選擇呢,下邊舉兩個例子:

在修改用戶的最後登錄時間,或者用戶的個人資料等數據時,這些數據都只有用戶自己登錄和登陸後纔會修改,不存在一個事務提交的信息被覆蓋的可能,所以這樣的業務我們就最低的隔離級別。

如果賬戶的餘額或者積分的消費,就可能存在多個客戶端同時消費一個賬戶的情況,此時我們應該選擇可重複讀隔離級別,來保證當一個客戶端在操作的時候,其他客戶端不能對該數據進行操作。

  1. 避免行鎖升級表鎖

我們知道,InnoDB 中行鎖是通過索引實現的,當不通過索引條件檢索數據時,行鎖就會升級成表鎖,我們知道表鎖會嚴重影響我們對整張表的操作,應該避免這種情況。

  1. 控制事務的大小,減少鎖定的資源和鎖定的時間

下邊這個 SQL 異常相比很多併發比較高的系統裏都會遇見,比如搶購系統的日誌中:

MySQLQueryInterruptedException: Query execution was interrupted

由於搶購系統中,提交訂單業務開啓了事務,在併發環境中對一條記錄進行更新操作的情況下,由於更新記錄所在的事務還可能存在其他操作,導致一個事務比較長,當大量請求進入時,就可能導致一些請求同時進入事務中,由於鎖的競爭是不公平的,當多個事務同時對一條記錄進行更新時,極端情況下,一個更新操作進去排隊系統後,可能會一直拿不到鎖,最後因超市被系統中斷,就會拋出上邊這個異常。

提交訂單需要創建訂單和扣減庫存,兩種不同順序的執行方式,結果都一樣,但是性能確實不一樣的:

這兩種不同的執行方式,雖然這些操作都在一個事務中,但是鎖的申請不在同一時間,鎖只有當其他操作都執行完成纔會釋放鎖。扣減庫存是更新操作,屬於行鎖,如果先扣減庫存會影響到其他操作該數據的事務,所以我們應該儘可能的避免長時間持有該鎖,儘快的釋放鎖。

因爲創建訂單和扣除庫存不管先執行哪一步都不影響業務,所以我們可以先執行新增操作,把扣除庫存放到最後,也就是使用執行順序 1 ,來減少鎖的持有時間。

總結

MySQL 的併發事務調優和 Java 的多線程編程調優非常類似,都是可以通過減小鎖粒度和減少鎖的持有時間進行調優。在 MySQL 的併發事務調優中,我們儘量在可以使用低事務隔離級別的業務場景中,避免使用高事務隔離級別。

在功能業務開發時,我們往往會爲了追求開發速度,習慣使用默認的參數設置來實現業務功能。例如,在 service 方法中,你可能習慣默認使用 transaction,很少再手動變更事務隔離級別。但要知道,transaction 默認是 RR 事務隔離級別,在某些業務場景下,可能並不合適。因此,我們還是要結合具體的業務場景,進行考慮。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vYYupZDrpqLb2XPLJ1E7sw