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

數據庫事務是訪問可能操做各類數據項的一個數據庫操做序列,這些操做要麼所有成功,要麼所有失敗。提起事務,你們都知道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是用來作事務回滾的,記錄了回滾的不一樣版本的行記錄)ide

鎖的具體實現算法

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 事務隔離級別,在某些業務場景下,可能並不合適。所以,咱們仍是要結合具體的業務場景,進行考慮。

相關文章
相關標籤/搜索