資料庫系列:InnoDB下實現高併發控制

Brand發表於2023-11-07

資料庫系列:MySQL慢查詢分析和效能最佳化
資料庫系列:MySQL索引最佳化總結(綜合版)
資料庫系列:高併發下的資料欄位變更
資料庫系列:覆蓋索引和規避回表
資料庫系列:資料庫高可用及無損擴容
資料庫系列:使用高區分度索引列提升效能
資料庫系列:字首索引和索引長度的取捨
資料庫系列:MySQL引擎MyISAM和InnoDB的比較

1 介紹

併發控制是為了防止多使用者併發使用資料庫時造成資料錯誤和程式執行錯誤,保證資料的完整性。當多個事務併發地存取資料庫時,就會產生同時讀取和/或修改同一資料的情況。若對併發操作不加控制就可能會存取和儲存不正確的資料,破壞資料庫的一致性(Consistency)。因此,資料庫中介軟體必須提供併發控制(Concurrency Control)機制能力,而MySQL的InnoDB引擎,很好的支援了這一塊。

2 InnoDB併發控制

MySQL 是一個流行的關係型資料庫管理系統,它支援多使用者併發訪問。併發控制是確保資料庫一致性和完整性的重要機制。在 MySQL 中,有幾種方法可以實施併發控制:

  • 讀寫鎖(Read-Write Locks):MySQL 使用了讀寫鎖來控制對資料的併發訪問。讀寫鎖是共享的,多個客戶端可以同時持有讀鎖,但只有一個客戶端可以持有寫鎖。當一個客戶端獲得寫鎖時,其他客戶端無法獲得讀鎖或寫鎖,直到寫鎖被釋放。
  • 事務隔離級別 :MySQL 提供了不同的事務隔離級別,包括讀未提交(Read Uncommitted)、讀已提交(Read Committed)、可重複讀(Repeatable Read)和序列化(Serializable)。較低的隔離級別允許更多的併發訪問,但可能導致資料不一致;較高的隔離級別可以確保資料一致性,但會限制併發訪問。
  • 鎖等待和死鎖:MySQL 提供了鎖等待和死鎖檢測機制。當一個事務嘗試獲取一個已經被其他事務持有的鎖時,MySQL 會阻塞等待,直到鎖被釋放。如果事務之間形成死鎖,MySQL 會檢測到並終止其中一個事務以解除死鎖。
  • 分段鎖定(Segmented Locking):MySQL 還支援分段鎖定,它允許對資料庫的特定部分進行鎖定,而不是對整個資料庫進行鎖定。這對於處理大型資料集時非常有用,因為它可以減少鎖定範圍,提高併發效能。
  • 樂觀併發控制(Optimistic Concurrency Control):MySQL 還支援樂觀併發控制,它假設衝突不太可能發生,因此不會立即鎖定資料。而是在更新時檢查是否存在衝突,如果存在衝突,則進行適當的處理,比如回滾或重試。
  • 多版本併發控制(MVCC):MVCC允許在事務隔離級別下執行一致性讀操作,以提高併發效能。

透過合理配置和使用上述機制,可以在 MySQL 中實現有效的併發控制,來保證在資料庫中執行一致性和資料完整性。
下面詳細來說說透過併發控制保證資料一致性的常見手段:

  • 鎖(Locking)
  • 資料多版本(Multi Versioning)

3 鎖的基本實現

MySQL 使用鎖來保持資料的一致性。在併發控制中,鎖是用來防止多個事務同時對同一資料進行修改或刪除,以保持資料的一致性。MySQL 中的鎖機制包括共享鎖和排他鎖。
鎖的基本實現,一般是這樣的:

  • 當使用者對資料進行操作前,鎖住,實施互斥,不允許其它任務的操作;
  • 當前操作完成後,釋放鎖之後,其他任務才可以執行;

但是存在一個問題,他的執行本質是序列的,無論讀寫,都無法並行,這樣效能太差了,也不符合網際網路高併發需求。
於是MySQL 中的鎖機制實現了共享鎖和排他鎖:

  • 共享鎖(Shared Lock):多個事務可以同時持有共享鎖,用於讀取資料,但不允許修改資料。共享鎖允許併發讀取,提高了併發效能。
  • 排他鎖(Exclusive Lock):只有一個事務可以持有排他鎖,用於修改資料,不允許其他事務同時修改。排他鎖會阻塞其他事務的讀寫操作,降低了併發效能。
    簡而言之就是:
  • 共享鎖之間不互斥,即讀讀可以並行,這樣提高了資料讀取的併發能力
  • 排他鎖與任何鎖互斥,所以寫讀,寫寫不可以並行,其他執行緒的讀寫操作都在鎖釋放之後才能執行,這樣對併發度是有較大影響的

所以說,單純的鎖機制,還是不滿足需求,為了保證寫任務沒有完成之時,其他讀的任務也可以併發執行,我們就需要使用另外一個能力來補充。
那就是資料多版本(Multi Versioning)

4 資料多版本的實現原理

MySQL的併發控制是透過多版本併發控制(MVCC)實現的。MVCC允許在事務隔離級別下執行一致性讀操作,以提高併發效能。
在MySQL的MVCC中,每個資料行都有多個版本,每個版本都表示該行在不同時間點的狀態。當一個事務讀取資料時,它只看到該事務開始之前存在的資料版本,而不是當前最新的資料版本。這種方式允許併發讀取多個資料版本,而不會相互阻塞,進一步提高併發的效果。
詳細拆分開來,讀寫同步執行的原理是這樣的:

  • 執行寫任務發時,Clone一份資料,打上新的版本號,與原版本號區分
  • 寫任務實際操作的是克隆那個版本的資料,直至操作並提交完成後
  • 讀任務可以併發執行,持續讀取,讀的是原版本的資料,並不會造成阻塞

image

如圖所示,可以分成這幾個步驟去解讀:

  1. 初始資料版本為V1.0
  2. T1發起了一個寫任務,這時候把資料clone了一份,進行修改,版本變為V2.0,這時候修改進行中,任務還未完成
  3. T2併發了一個讀任務,依然讀的是V1.0版本的資料
  4. T3又併發了一個讀任務,依然不會阻塞,讀的還是V1.0的版本
  5. 這時候資料修改,V1.0的資料變為V2.0的資料
  6. T4這時候發起的度任務,讀到的就是V2.0的資料了

從這邊可以看出,資料多版本,讀寫之間不需要阻塞,能夠極大提高任務的併發能力。

  • 普通意義上的鎖機制,本質是序列執行,效率十分低下
  • 讀寫鎖,可以實現讀讀併發,但是寫讀依然是互斥的,也不符合網際網路的機制
  • 資料多版本(Multi Versioning),才是實現讀寫併發的要素

5 MySQL 資料多版本的相關實現

5.1 概念介紹

在MySQL的InnoDB儲存引擎中,使用MVCC(多版本併發控制,Multiversion Concurrency Control)實現多版本控制。
MVCC的實現主要基於undo日誌、redo日誌、rollback segment 回滾儲存區間、和read view。undo日誌用於回滾操作,而read view用於生成資料行的歷史版本。透過這種方式,InnoDB實現了非阻塞的一致性讀操作。

  • redo日誌
    資料庫事務提交後,必須將更新的資料刷到磁碟上,以保證ACID特性。磁碟隨機寫效能較低,且過度頻繁的刷盤,會極大影響資料庫的吞吐量。
    最佳化方式是將修改行為先寫到redo日誌裡,這樣隨機就變成了有序性,再按照時間週期將資料持久化到磁碟上,極大提高了效能。
    另外一方面,即使資料庫崩潰,恢復之後也可以從redo日誌裡面獲取操作Log,重新提交事務操作,然後刷盤,最終保證資料的一致性。

  • undo日誌
    資料庫事務未提交時,會將事務修改資料的Mirror Data(修改前的版本 )存放到 Undo Log中,它的主要作用是在事務執行過程中,如果發生錯誤或者需要回滾操作,可以透過Undo Log中的記錄來撤銷已經執行的操作,恢復資料到事務開始之前的狀態。
    另外一方面,資料庫奔潰時,也可以使用undo日誌,撤銷未提交事務,保證事務的ACID特性。

    • insert操作:undo日誌儲存新資料的PK(ROW_ID),回滾時執行刪除即可。
    • delete/update操作:undo日誌儲存舊資料row(整行資料),回滾時直接恢復。
  • rollback segment
    Rollback Segment(回滾儲存區)是資料庫中的一部分儲存空間,用於臨時儲存當資料庫資料發生改變時的先前值。它主要有兩個作用:

    • 透過Rollback操作來取消資料操作,使之恢復到改變之前的原始值,在transaction的過程才有效。如果執行了commit命令,那麼Rollback Segment裡面的值就會標識為失效,資料的改變將永久化。
    • select讀取的同時另一個事務也在修改這個表的值,那麼select出來的資料是修改前的值,因為修改之前的原資料存入到了Rollback Segment中,所以不會被阻塞到。

5.2 示例說明

5.2.1 初始資料

# 表結構
t_userinfo(id PK, name, sex, age);

# 預設資料
1,Brand,0,22
2,Helenlyn,1,19
3,Sol,0,21

image

先初始化一個預設的表,裡面模擬幾條資料。此時沒有任何的事務未提交操作,所以回滾段是空的,如上圖。

5.2.2 事務操作示例

start transaction;
delete from t_userinfo where id = 1;
update t_userinfo set name = 'Helenlyn...' where id = 2;
insert  into t_userinfo(name, sex, age) valus ('Lili', 1, 18);

還未執行commit 或者 rollback,所以事務處於未提交的狀態

image

綜上,我們可以看出Commit之前我們進行如下操作:

  • 正式提交刪除前,id=1的資料作為舊版本的資料,進入了回滾儲存區;
  • 正式提交修改前,id=2的資料作為舊版本的資料,進入了回滾儲存區;
  • 新插入的資料 ('Lili', 1, 18), id= 4,在正式提交之前,也進入了回滾段;

我們上面說了,不是所有的操作最終都會commit,如果失敗,事務rollback,就可以透過回滾儲存區中的undo日誌對操作進行回滾。

如果成功commit,則整體提交成功了
image
可以看到:

  • id=1 資料刪除成功;
  • id=2 欄位更新成功;
  • id=4 資料行插入成功;
  • 回滾儲存區相關日誌清掉

如果失敗並執行rollback,則全部回滾
image

  • 資料刪除的恢復了
  • 被修改的舊資料也恢復
  • 新增寫入的資料刪除
  • 回滾儲存區相關日誌清掉

6 總結

  • MySQL實現併發控制,保證資料一致性的方法有鎖,資料多版本等
  • 普通鎖序列,讀寫鎖讀讀並行,Multi Versioning 讀寫並行;
  • redo日誌保證已提交事務的ACID特性, undo日誌用來回滾未提交的事務,rollback segment 為臨時回滾儲存區;
  • InnoDB是基於多版本併發控制的儲存引擎;
  • InnoDB用的多版本是快照讀不加鎖,所有select都是快照讀,這些資料不會被修改,併發效能特別高;

相關文章