一文詳解MySQL中的事務與 MVCC 原理

青灯夜游
發布: 2022-03-09 11:05:01
轉載
2543 人瀏覽過

這篇文章帶大家了解MySQL中的事務,並介紹一下MVCC 原理,希望能提供協助給大家!

一文詳解MySQL中的事務與 MVCC 原理

01 什麼是交易?

資料庫事務指的是一組資料操作,事務內的操作要嘛就是全部成功,要嘛就是全部失敗,什麼都不做,其實不是沒做,是可能做了一部分但是只要有一步失敗,就要回滾所​​有操作,有點一不做二不休的意思。

在 MySQL 中,事務支援是在引擎層實現的。 MySQL 是一個支援多引擎的系統,但並不是所有的引擎都支援事務。 例如 MySQL 原生的 MyISAM 引擎就不支援事務,這也是 MyISAM 被 InnoDB 取代的重要原因之一

1.1 四大特性

  • #原子性(Atomicity):事務開始後所有操作,要麼全部做完,要麼全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。
  • 一致性(Consistency):事務開始前和結束後,資料庫的完整性限制並沒有被破壞 。例如 A 向 B 轉賬,不可能 A 扣了錢,B 卻沒收到。
  • 隔離性(Isolation):同一時間,只允許一個交易請求同一數據,不同的事務之間彼此沒有任何干擾。例如 A 正在從一張銀行卡中取錢,在 A 取錢的過程結束前,B 不能向這張卡轉帳。
  • 持久性(Durability):事務完成後,事務對資料庫的所有更新將被保存到資料庫,不能回滾。

1.2 隔離等級

SQL 交易的四大特性中原子性、一致性、持久性都比較好理解。但事務的隔離等級確實比較難的,今天主要聊聊 MySQL 事務的隔離性。

SQL 標準的交易隔離從低到高階依序是:讀取未提交(read uncommitted)、讀取提交(read committed)、可重複讀取(repeatable read)和串列化(serializable )。等級越高,效率越低

  • 讀取未提交:當一個交易還沒提交時,它所做的變更就能被別的交易看到。
  • 讀取提交:一個交易提交之後,它所做的變更才會被其他交易看到。
  • 可重複讀取:一個事務執行過程中看到的數據,總是跟著這個事務在啟動時看到的數據是一致的。當然在可重複讀取隔離等級下,未提交變更對其他交易也是不可見的。
  • 串行化:顧名思義是對於同一行記錄,“寫” 會加 “寫鎖”,“讀” 會加 “讀鎖”。當出現讀寫鎖定衝突的時候,後存取的事務必須等前一個事務執行完成,才能繼續執行。 所以種隔離等級下所有的資料是最穩定的,但是效能也是最差的

1.3 解決的並發問題

SQL 交易隔離等級的設計就是為了能最大限度的解決並發問題:

  • 髒讀:事務A 讀取了事務B 更新的數據,然後B 回滾操作,那麼A 讀取到的數據是髒數據
  • 不可重複讀:事務A 多次讀取相同數據,事務B 在事務A 多次讀取的過程中,對數據作了更新並提交,導致事務A 多次讀取同一數據時,​​結果不一致。
  • 幻讀:系統管理員A 將資料庫中所有學生的成績從具體分數改為ABCDE 等級,但是系統管理員B 就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一筆記錄沒有改過來,好像發生了幻覺一樣,這就叫幻讀。

SQL 不同的交易隔離等級能解決的並發問題也不一樣,如下表所示:只有串列化的隔離等級解決了全部這3 個問題,其他的3個隔離等級都有缺陷

讀取未提交可能可能讀取已提交不可能可能可能#可重複讀取#可能
交易隔離等級 髒讀 #無法重複讀取 幻讀
########## #不可能######可能############序列化######不可能######不可能######不可能# ###########

PS:不可重複讀的和幻讀很容易混淆,不可重複讀著重於修改,幻讀著重於新增或刪除。解決不可重複讀取的問題只需鎖定滿足條件的行,解決幻讀需要鎖定表

1.4 舉個栗子

這麼說可能有點難懂,舉個栗子。還是之前的表格結構以及表格資料

CREATE TABLE `student`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
登入後複製

一文詳解MySQL中的事務與 MVCC 原理

#假設現在,我要同時啟動兩個食物,一個交易A 查詢id = 2 的學生的age,一個事務B 更新id = 2 的學生的age。流程如下,在四種隔離等級下的 X1、X2、X3 的值分別是怎樣的呢?

一文詳解MySQL中的事務與 MVCC 原理

  • 讀取未提交:X1 的值是23,因為交易B 雖然沒有提交但它的變更已被A看到。 (如果 B 後面又回滾了 X1 的值就是髒的)。 X2、X3 的值也是 23,這無可厚非。
  • 讀取已提交:X1 的值是 22,因為 B 雖然改了,但 A 看不到。 (如果 B 後面回滾了,X1 的值不變,解決了髒讀),X2、X3 的值是 23,沒毛病,B 提交了,A 才能看到。
  • 可重複讀取:X1、X2 都是 22,A 開啟的時刻值是 22,那麼在 A 的整個過程中,它的值都是 22。 (不管B 在這段期間怎麼修改,只要A 還沒提交,都是看不見的,解決了不可重複讀),而X3 的值是23,因為A 提交了,能看到B修改的值了。
  • 串行化:B 在執行變更期間會被鎖住,直到 A 提交。 B 才能繼續執行。 (A 在讀取期間,B 不能寫。得保證此時資料是最新的。解決了幻讀)所以X1、X2 都是22,而最後的X3 在B 提交之後執行,它的值就是23。

那為什麼會出現這樣的結果呢?事務隔離等級到底是怎麼實現的呢?

交易隔離等級是怎麼是實現的呢?我在極客時間丁奇老師的課上找到了答案:

實際上,資料庫裡面會創建一個視圖,訪問的時候以視圖的邏輯結果為準。 在 「可重複讀取」 隔離等級下,這個視圖是在事務啟動時建立的,整個事務存在期間都會使用這個視圖。在 “讀取提交” 隔離等級下,這個視圖是在每個 SQL 語句開始執行的時候建立的。這裡要注意的是,「讀取未提交」 隔離等級下直接傳回記錄上的最新值,沒有視圖概念;而 「串列化」 隔離等級下直接用加鎖的方式來避免並行存取

1.5 設定交易隔離等級

#不同的資料庫預設設定的交易隔離等級也大不一樣,Oracle 資料庫的預設隔離等級是讀取提交,而MySQL 是可重複讀取。所以,當你的系統需要把資料庫從 Oracle 遷移到 MySQL 時,請把等級設定成與搬遷之前的(讀取提交)一致,避免出現不可預測的問題

1.5.1 檢視交易隔離等級

# 查看事务隔离级别
5.7.20 之前
SELECT @@transaction_isolation
show variables like 'transaction_isolation';

# 5.7.20 以及之后
SELECT @@tx_isolation
show variables like 'tx_isolation'

+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
登入後複製

1.5.2 設定隔離等級

#修改隔離等級語句格式是:set [作用域] transaction isolation level [交易隔離等級]

其中作用域可選:SESSION(會話)、GLOBAL(全域);隔離級別就是上面提到的4 種,不區分大小寫。

例如:設定全域隔離等級為讀取提交

set global transaction isolation level read committed;
登入後複製

1.6 交易的啟動

MySQL 的交易啟動有以下幾種方式:

  • 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,或者回滚语句是 rollback。
# 更新学生名字
START TRANSACTION;
update student set name = '张三' where id = 2;
commit;
登入後複製
  • set autocommit = 0,这个命令会将线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
  • set autocommit = 1,表示 MySQL 自动开启和提交事务。 比如执行一个 update 语句,语句只完成后就自动提交了。不需要显示的使用 begin、commit 来开启和提交事务。所以当我们执行多个语句的时候,就需要手动的用 begin、commit 来开启和提交事务。
  • start transaction with consistent snapshot;上面提到的 begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 命令。 第一种启动方式,一致性视图是在执行第一个快照读语句时创建的; 第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的

02 事务隔离的实现

理解了隔离级别,那事务的隔离是怎么实现的呢?要想理解事务隔离,先得了解 MVCC 多版本的并发控制这个概念。而 MVCC 又依赖于 undo log 和 read view 实现。

2.1 什么是 MVCC?

百度上的解释是这样的:

MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

MVCC 使得数据库读不会对数据加锁,普通的 SELECT 请求不会加锁,提高了数据库的并发处理能力;数据库写才会加锁。 借助 MVCC,数据库可以实现 READ COMMITTED,REPEATABLE READ 等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了 ACID 中的 I 特性(隔离性)。

MVCC 只在 REPEATABLE READ 和 READ COMMITIED 两个隔离级别下工作。其他两个隔离级别都和 MVCC 不兼容 ,因为 READ UNCOMMITIED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

2.1.1 InnDB 中的 MVCC

InnDB 中每个事务都有一个唯一的事务 ID,记为 transaction_id。它在事务开始时向 InnDB 申请,按照时间先后严格递增。

而每行数据其实都有多个版本,这就依赖 undo log 来实现了。每次事务更新数据就会生成一个新的数据版本,并把  transaction_id 记为 row trx_id。同时旧的数据版本会保留在 undo log 中,而且新的版本会记录旧版本的回滚指针,通过它直接拿到上一个版本。

所以,InnDB 中的 MVCC 其实是通过在每行记录后面保存两个隐藏的列来实现的。一列是事务 ID:trx_id;另一列是回滚指针:roll_pt。

2.2 undo log

回滚日志保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。

根据操作的不同,undo log 分为两种: insert undo log 和 update undo log。

2.2.1 insert undo log

insert 操作产生的 undo log,因为 insert 操作记录没有历史版本只对当前事务本身可见,对于其他事务此记录不可见,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作。

purge 的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收 undo pages

所以,插入数据时。它的初始状态是这样的:

insert undo log

2.2.2 update undo log

UPDATE 和 DELETE 操作产生的 Undo log 都属于同一类型:update_undo。(update 可以视为 insert 新数据到原位置,delete 旧数据,undo log 暂时保留旧数据)。

事务提交时放到 history list 上,没有事务要用到这些回滚日志,即系统中没有比这个回滚日志更早的版本时,purge 线程将进行最后的删除操作。

一個交易修改目前資料:

一文詳解MySQL中的事務與 MVCC 原理

另一個交易修改資料:

一文詳解MySQL中的事務與 MVCC 原理

#這樣的同一筆記錄在資料庫中存在多個版本,就是上面提到的多版本並發控制MVCC。

另外,借助 undo log 透過回溯可以回到上一個版本狀態。例如要回到 V1 只需要順序執行兩次回滾即可。

2.3 read-view

#read view 是InnDB 在實作MVCC 時用到的一致性讀取視圖,用於支持RC(讀取提交)以及RR(可重複讀取)隔離等級的實作

read view 不是真實存在的,只是一個概念,undo log 才是它的體現。它主要是透過版本和 undolog 計算出來的。作用是決定事務能看到哪些資料

每個交易或語句有自己的一致性檢視。普通查詢語句是一致性讀,一致性讀會根據 row trx_id 和一致性檢視來決定資料版本的可見性

2.3.1 資料版本的可見性規則

#read view 中主要包含目前系統中還有哪些活躍的讀寫事務,在實作上InnDB 為每個事務建構了一個數組,用來保存這個事務啟動瞬間,當前正活躍(還未提交)的事務

前面說了事務ID 隨時間嚴格遞增的,把系統中已提交的事務ID 的最大值記為數組的低水位,已創建過的事務ID 1記為高水位

這個視圖陣列和高水位就組成了目前事務的一致性視圖(read view)

這個陣列畫個圖,長這樣:

一文詳解MySQL中的事務與 MVCC 原理

規則如下:

  • 1 如果trx_id 在灰色區域,表示被存取版本的trx_id 小於陣列中低水位的id 值,也即產生該版本的事務在生成read view 前已經提交,所以該版本可見,可以被當前事務存取。
  • 2 如果trx_id 在橘色區域,表示被存取版本的trx_id 大於數組中高水位的id 值,也即生成該版本的事務在生成read view 後才生成,所以該版本不可見,不能被目前事務存取。
  • 3 如果在綠色區域,就會有兩種情況:

    • a) trx_id 在陣列中,證明這個版本是由尚未提交的交易產生的,不可見
    • b) trx_id 不在數組中,證明這個版本是由已提交的事務生成的,可見

第三點我在看教程的時候也有點疑惑,好在有熱心網友解答:

落在綠色區域意味著是事務ID 在低水位和高水位這個範圍裡面,而真正是否可見,看綠色區域是否有這個值。如果綠色區域沒有這個交易 ID,則可見,如果有,則不可見。在這個範圍裡面並不代表這個範圍就有這個數值,例如 [1,2,3,5],4 在這個陣列 1-5 的範圍裡,卻沒在這個陣列裡面。

這樣說可能有點難以理解,我假設一個場景:三個事務對同一條資料進行查詢更新等操作,為此畫了張圖以方便理解:

一文詳解MySQL中的事務與 MVCC 原理

原始資料還是下圖這樣的,對id = 2 的張三進行資訊的更新:

一文詳解MySQL中的事務與 MVCC 原理

針對上圖,我想提個問題。 分別在 RC(讀取提交)以及 RR(可重複讀取)隔離等級下,T4 和 T5 時間點的查詢 age 值分別是多少呢? T4 更新的值又是多少呢? 思考片刻,相信大家都有自己的答案。答案在文末,希望大家能帶著自己的疑問繼續讀下去。

2.3.2 RR(可重複讀取)下的結果

RR 層級下,查詢只承認在交易啟動前就已經提交完成的數據,一旦啟動事務就會建立視圖。所以使用 start transaction with consistent snapshot 指令,馬上就會建構視圖

现在假设:

  • 事务 A 开始前,只有一个活跃的事务,ID = 2,
  • 已提交的事务也就是插入数据的事务 ID = 1
  • 事务 A、B、C 的事务 ID 分别是 3、4、5

在这种隔离级别下,他们创建视图的时刻如下:

一文詳解MySQL中的事務與 MVCC 原理

根据上图得,事务 A 的视图数组是[2,3];事务 B 的视图数组是 [2,3,4];事务 C 的视图数组是[2,3,4,5]。分析一波:

  • T4 时刻,B 读数据都是从当前版本读起,过程是这样的:

    • 读到当前版本的 trx_id = 4,刚好是自己,可见
    • 所以 age = 24
  • T5 时刻,A 读数据都是从当前版本读起,过程是这样的:

    • 读到当前版本的 trx_id = 4,比自己视图数组的高水位大,不可见
    • 再往上读到 trx_id = 5,比自己视图数组高水位大,不可见
    • 再往上读到 trx_id = 1,比自己视图数组低水位小,可见
    • 所以 age = 22

这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读

其实视图是否可见主要看创建视图和提交的时机,总结下规律:

  • 版本未提交,不可见
  • 版本已提交,但在视图创建后提交,不可见
  • 版本已提交,但在视图创建前提交,可见

2.3.2.1 快照读和当前读

事务 B 的 update 语句,如果按照上图的一致性读,好像结果不大对?

如下图周明,B 的视图数组是先生成的,之后事务 C 才提交。那就应该看不见 C 修改的 age = 23 呀?最后 B 怎么得出 24 了?

一文詳解MySQL中的事務與 MVCC 原理

没错,如果 B 在更新之前执行查询语句,那返回的结果肯定是 age = 22。问题是更新就不能在历史版本更新了呀,否则 C 的更新不就丢失了?

所以,更新有个规则:更新数据都是先读后写(读是更新语句执行,不是我们手动执行),读的就是当前版本的值,叫当前读;而我们普通的查询语句就叫快照读

因此,在更新时,当前读读到的是 age = 23,更新之后就成 24 啦。

2.3.2.2 select 当前读

除了更新语句,查询语句如果加锁也是当前读。如果把事务 A 的查询语句 select age from t where id = 2 改一下,加上锁(lock in mode 或者 for update),也都可以得到当前版本 4 返回的 age = 24

下面就是加了锁的 select 语句:

select age from t where id = 2 lock in mode;
 select age from t where id = 2 for update;
登入後複製

2.3.2.3 事务 C 不马上提交

假设事务 C 不马上提交,但是 age = 23 版本已生成。事务 B 的更新将会怎么走呢?

事务 C 不马上提交

事务 C 还没提交,写锁还没释放,但是事务 B 的更新必须要当前读且必须加锁。所以事务 B 就阻塞了,必须等到事务 C 提交,释放锁才能继续当前的读。

被事务 C 锁住

2.3.3 RC(读提交)下的结果

在读提交隔离级别下,查询只承认在语句启动前就已经提交完成的数据;每一个语句执行之前都会重新算出一个新的视图

注意:在上图的表格中用于启动事务的是 start transaction with consistent snapshot 命令,它会创建一个持续整个事务的视图。所以,在  RC 级别下,这命令其实不起作用。等效于普通的 start transaction(在执行 sql 语句之前才算是启动了事务)。所以,事务 B 的更新其实是在事务 C 之后的,它还没真正启动事务,而 C 已提交

现在假设:

  • 事務A 開始前,只有一個活躍的事務,ID = 2,
  • 已提交的事務也就是插入資料的事務ID = 1
  • 事務A、B 、C 的事務ID 分別是3、4、5

在這種隔離層級下,他們建立視圖的時刻如下:

一文詳解MySQL中的事務與 MVCC 原理

根據上圖得,事務A 的視圖數組是[2,3,4],但它的高水位是6或更大(已創建事務ID 1);事務B 的視圖數組是[ 2,4];事務C 的視圖陣列是[2,5]。分析一波:

  • T4 時刻,B 讀資料都是從目前版本讀起,過程是這樣的:

      ##讀到目前版本的trx_id = 4,剛好是自己,可見
    • 所以age = 24
  • #T5 時刻,A 讀資料都是從目前版本讀起,過程是這樣的:

      讀到目前版本的trx_id = 4,在自己一致性視圖範圍內但包含4,不可見
    • 再往上讀到trx_id = 5,在自己一致性視圖範圍內但不包含5,可見
    • 所以age = 23

03巨人的肩膀

    cnblogs.com/wyaokai/p/10921323.html
  • time.geekbang.org/column/article/70562
  • zhuanlan.zhihu.com/p/117476959
  • cnblogs.com/xd502djj/p/6668632.html
  • blog.csdn.net/article/details/109044141
  • blog.csdn.net/u014078930/article/details/#blog.csdn.net/u014078930/article 99659272

04 總結

本文詳細聊了交易的方方面面,例如:四大特性、隔離等級、解決的並發問題、如何設定、查看隔離等級、如何啟動交易等。除此之外,還深入了解了 RR 和 RC 兩個層級的隔離是怎麼實現的?包括詳解 MVCC、undo log 和 read view 是怎麼配合實現 MVCC 的。最後還聊了快照讀、目前讀等等。可以說,事務相關的知識點都在這裡了。看完這篇還不懂的話,你來捶我呀!

【相關推薦:

mysql影片教學#

以上是一文詳解MySQL中的事務與 MVCC 原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:segmentfault.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板