MySQL事務控制和鎖機制

摘要

本文基於MySQL5.7爲基礎,討論與數據庫事務和鎖的相關內容。

鎖機制

根據加鎖的範圍,MySQL裏面的鎖可以分成全局鎖表級鎖行鎖三類。

全局鎖

全局鎖能夠對整個庫實例進行加鎖。

加鎖的語法:

FLUSH TABLES WITH READ LOCK;

解鎖的語法:

UNLOCK TABLES;

全局鎖的典型使用場景是,做全庫邏輯備份。應用全局鎖做邏輯備份有以下問題:

  • 如果你在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就得停擺;

  • 如果你在從庫上備份,那麼備份期間從庫不能執行主庫同步過來的binlog,會導致主從延遲。

MySQL官方邏輯備份工具mysqldump,以下參數代表不通的實現方式:

  • --lock-all-tables,在整個轉儲期間獲取全局鎖定來實現的。適合全庫邏輯備份。

  • --lock-tables,在轉儲之前鎖定所有要轉儲的表。適合多表邏輯備份。

  • --single-transaction導數據之前就會啓動一個事務,來確保拿到一致性視圖,因此需要將事務隔離模式設置爲REPEATABLE READ。適合REPEATABLE READ的隔離級別,並且表支持事務。

表鎖

對一個或者多個表進行加鎖。

加鎖的語法:

LOCK TABLES
tbl_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}
[, tbl_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}] ...

解鎖的語法:

UNLOCK TABLES;

在還沒有出現更細粒度的鎖的時候,表鎖是最常用的處理併發的方式。而對於InnoDB這種支持行鎖的引擎,一般不使用lock tables命令來控制併發,畢竟鎖住整個表的影響面還是太大。

讀鎖之間不互斥,因此你可以有多個線程同時對一張表增刪改查。讀寫鎖之間、寫鎖之間是互斥的,用來保證安全性。

行鎖

行鎖,是現階段InnoDB引擎支持的鎖,因此,常常伴隨着事務一同出現。在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。

讀鎖 寫鎖
讀鎖 兼容 衝突
寫鎖 衝突 衝突

事務控制

事務特性

ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性)

  • 原子性:整個事務中的所有操作,要麼全部完成,要麼全部不完成。
  • 一致性:事務必須始終保持系統處於一致的狀態。
  • 隔離性:隔離狀態執行事務,使它們好像是系統在給定時間內執行的唯一操作。
  • 持久性:在事務完成以後,該事務對數據庫所作的更改便持久的保存在數據庫之中,並不會被回滾。

隔離級別

  • 讀未提交是指,一個事務還沒提交時,它做的變更就能被別的事務看到。
  • 讀已提交是指,一個事務提交之後,它做的變更纔會被其他事務看到。
  • **可重複讀(默認)**是指,一個事務執行過程中看到的數據,總是跟這個事務在啓動時看到的數據是一致的。當然在可重複讀隔離級別下,未提交變更對其他事務也是不可見的。
  • 串行化,顧名思義是對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

事務應用

MySQL通過SET AUTOCOMMIT, START TRANSACTION, COMMIT和ROLLBACK等語句支持本地事務。

語法:

START TRANSACTION | BEGIN [WORK]

COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]

ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]

SET AUTOCOMMIT = {0 | 1}

默認情況下,mysql是autocommit的,如果需要通過明確的commit和rollback來提交和回滾事務,那麼需要通過明確的事務控制命令來開始事務。
START TRANSACTION或BEGIN語句可以開始一項新的事務。
COMMIT和ROLLBACK用來提交或者回滾事務。

手動提交與自動提交

如果我們只是對某些語句需要進行事務控制,則使用START TRANSACTION開始一個事務比較方便,這樣事務結束之後可以自動回到自動提交的方式,如果我們希望我們所有的事務都不是自動提交的,那麼通過修改AUTOCOMMIT來控制事務比較方便,這樣不用在每個事務開始的時候再執行START TRANSACTION。

time session_1 session_2
mysql> SELECT * FROM t1;
Empty set
mysql> SELECT * FROM t1;
Empty set
mysql> start transaction;
Query OK, 0 rows affected

mysql> insert into t1
values(‘1’,1);
Query OK, 1 row affected
mysql> SELECT * FROM t1;
Empty set
mysql> commit;
Query OK, 0 rows affected
手動提交
mysql> SELECT * FROM t1;
±----±----+
| a | b |
±----±----+
| 1 | 1 |
±----±----+
1 row in set
mysql> insert into t1
values(‘2’,2);
Query OK, 1 row affected
自動提交
mysql> SELECT * FROM t1;
±----±----+
| a | b |
±----±----+
| 1 | 1 |
| 2 | 2 |
±----±----+
2 row in set

開始一個事務,會造成一個隱含的unlock tables被執行:

time session_1 session_2
mysql> select * from t1;
Empty set
mysql> select * from t1;
Empty set
mysql> lock table t1
write;
Query OK, 0 rows affected
mysql> select * from t1;
waiting…
mysql> insert into t1 values(‘1’,1);
Query OK, 1 row affected
waiting…
mysql> rollback;
Query OK, 0 rows affected
waiting…
mysql> start transaction;
Query OK, 0 rows affected
開始一個事務時,表鎖被釋放。
waiting…
Waiting ending
mysql> select * from t1;
±----±----+
| a | b |
±----±----+
| 1 | 1 |
±----±----+
1 row in set
對於已經提交的事務(rollback之前是自動提交),
不能通過 rollback進行回滾。

行鎖

如果兩個事務對同一行數據進行更新,由於InnoDB會對該行數據進行加鎖,所以只能一個事務獲得鎖,另一個事務必須等待。

time session_1 session_2
mysql> select * from t1;
±----±----+
| a | b |
±----±----+
| 1 | 1 |
±----±----+
1 row in set
mysql> select * from t1;
±----±----+
| a | b |
±----±----+
| 1 | 1 |
±----±----+
1 row in set
update t1 set a = a+1;
Query OK, 1 row affected
Rows matched: 1 Changed: 1 Warnings: 0
update t1 set a = a+1;
waiting…
commit;
Query OK, 0 rows affected
waiting ending
update t1 set a = a+1;
Query OK, 1 row affected
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t1;
±----±----+
| a | b |
±----±----+
| 2 | 1 |
±----±----+
1 row in set
mysql> select * from t1;
±----±----+
| a | b |
±----±----+
| 3 | 1 |
±----±----+
1 row in set

MVCC

InnoDB實現了多版本併發控制(MVCC),這意味着不同的用戶將看到與之交互的數據的不同版本。這樣做是爲了使用戶能夠看到系統的一致性視圖,而沒有昂貴且性能受限的鎖定,而鎖定會限制併發性。

在InnoDB的MVCC實現中,需要知道的關鍵一點是,當一個記錄被修改時,被修改的數據的當前(“舊”)版本首先作爲一個“undo record”隱藏在一個“undo log”中。它之所以稱爲撤銷日誌,是因爲它包含撤銷用戶所做更改所需的信息,從而將記錄還原爲以前的版本。

在這裏插入圖片描述

每個記錄都包含對其最新撤消記錄的引用(稱爲回滾指針),每個撤消記錄都包含對其以前撤消記錄的引用(除了初始記錄插入),形成記錄鏈。這樣,只要撤消記錄(“歷史”)仍然存在於撤消日誌中,就可以輕鬆地構造記錄的任何早期版本。

可重複讀

正是MVCC提供的一致性視圖,使InnoDB支持可重複讀。下面來看看它具體是怎麼實現的。

InnoDB每一個事務都有一個唯一的id,叫做transaction id,它是在事務開始的時候向InnoDB的事務系統申請的,是按申請順序嚴格遞增的。

每行數據也都是有多個版本的。每次事務更新數據的時候,都會生成一個新的數據版本,並且把transaction id賦值給這個數據版本的事務ID,記爲row trx_id。同時,舊的數據版本要保留,並且在新的數據版本中,能夠有信息可以直接拿到它。

在這裏插入圖片描述

圖中虛線框裏是同一行數據的4個版本,當前最新版本是V4,k的值是22,它是被transaction id 爲25的事務更新的,因此它的row trx_id也是25。

圖中虛線框裏是同一行數據的4個版本,當前最新版本是V4,k的值是22,它是被transaction id 爲25的事務更新的,因此它的row trx_id也是25。實際上,圖中的三個虛線箭頭,就是undo log;而V1、V2、V3並不是物理上真實存在的,而是每次需要的時候根據當前版本和undo log計算出來的。比如,需要V2的時候,就是通過V4依次執行U3、U2算出來。

InnoDB爲每個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正在“活躍”的所有事務ID。“活躍”指的就是,啓動了但還沒提交。

在這裏插入圖片描述

對於當前事務的啓動瞬間來說,一個數據版本的row trx_id,有以下幾種可能:

  • 如果trx_id屬於已提交事務,表示這個版本是已提交的事務或者是當前事務自己生成的,這個數據可見;
  • 如果trx_id屬於未開始事務,表示這個版本是由將來啓動的事務生成的,是肯定不可見的;
  • 如果trx_id屬於未提交事務集合,那就包括兩種情況:
  1. 若trx_id在數組中,表示這個版本是由還沒提交的事務生成的,不可見;
  2. 若trx_id不在數組中,表示這個版本是已經提交了的事務生成的,可見。

InnoDB利用了“所有數據都有多個版本”的這個特性,實現了“秒級創建快照”的能力。

分析下事務A的語句返回的結果,爲什麼是k=1。

在這裏插入圖片描述

現在事務A要來讀數據了,它的視圖數組是[99,100]。當然了,讀數據都是從當前版本讀起的。所以,事務A查詢語句的讀數據流程是這樣的:

找到(1,3)的時候,判斷出row trx_id=101,比高水位大,處於紅色區域,不可見;

接着,找到上一個歷史版本,一看row trx_id=102,比高水位大,處於紅色區域,不可見;

再往前找,終於找到了(1,1),它的row trx_id=90,比低水位小,處於綠色區域,可見。

更新邏輯

事務B的update語句,如果按照一致性讀,好像結果不對哦?

事務B的視圖數組是先生成的,之後事務C才提交,不是應該看不見(1,2)嗎,怎麼能算出(1,3)?

在這裏插入圖片描述

這裏就用到了這樣一條規則:更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲“當前讀”(current read)。

其實,除了update語句外,select語句如果加鎖(加上lock in share mode 或 for update),也是當前讀。

所以,能算出(1,3)。

讀已提交

下面是讀提交時的狀態圖,可以看到這兩個查詢語句的創建視圖數組的時機發生了變化,就是圖中的read view框。

在這裏插入圖片描述

在讀已提交的隔離級別下,所有已提交的版本的數據都能被讀到,不會提供一致性讀。read view是在get的時候才被創建的,所以能都到所有已提交的數據。

參考

[1] [eimhe.com]網易技術部的MySQL中文資料.

[2] MySQL 5.7 Reference Manual

[3] MySQL實戰45講

[4] The basics of the InnoDB undo logging and history system

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章