這篇文章帶大家了解MySQL中的事務,並介紹一下MVCC 原理,希望能提供協助給大家!
資料庫事務指的是一組資料操作,事務內的操作要嘛就是全部成功,要嘛就是全部失敗,什麼都不做,其實不是沒做,是可能做了一部分但是只要有一步失敗,就要回滾所有操作,有點一不做二不休的意思。
在 MySQL 中,事務支援是在引擎層實現的。 MySQL 是一個支援多引擎的系統,但並不是所有的引擎都支援事務。 例如 MySQL 原生的 MyISAM 引擎就不支援事務,這也是 MyISAM 被 InnoDB 取代的重要原因之一。
1.1 四大特性
1.2 隔離等級
SQL 交易的四大特性中原子性、一致性、持久性都比較好理解。但事務的隔離等級確實比較難的,今天主要聊聊 MySQL 事務的隔離性。
SQL 標準的交易隔離從低到高階依序是:讀取未提交(read uncommitted)、讀取提交(read committed)、可重複讀取(repeatable read)和串列化(serializable )。等級越高,效率越低。
1.3 解決的並發問題
SQL 交易隔離等級的設計就是為了能最大限度的解決並發問題:
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;
#假設現在,我要同時啟動兩個食物,一個交易A 查詢id = 2 的學生的age,一個事務B 更新id = 2 的學生的age。流程如下,在四種隔離等級下的 X1、X2、X3 的值分別是怎樣的呢?
那為什麼會出現這樣的結果呢?事務隔離等級到底是怎麼實現的呢?
交易隔離等級是怎麼是實現的呢?我在極客時間丁奇老師的課上找到了答案:
實際上,資料庫裡面會創建一個視圖,訪問的時候以視圖的邏輯結果為準。 在 「可重複讀取」 隔離等級下,這個視圖是在事務啟動時建立的,整個事務存在期間都會使用這個視圖。在 “讀取提交” 隔離等級下,這個視圖是在每個 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 的交易啟動有以下幾種方式:
# 更新学生名字 START TRANSACTION; update student set name = '张三' where id = 2; commit;
理解了隔离级别,那事务的隔离是怎么实现的呢?要想理解事务隔离,先得了解 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
所以,插入数据时。它的初始状态是这样的:
2.2.2 update undo log
UPDATE 和 DELETE 操作产生的 Undo log 都属于同一类型:update_undo。(update 可以视为 insert 新数据到原位置,delete 旧数据,undo log 暂时保留旧数据)。
事务提交时放到 history list 上,没有事务要用到这些回滚日志,即系统中没有比这个回滚日志更早的版本时,purge 线程将进行最后的删除操作。
一個交易修改目前資料:
另一個交易修改資料:
#這樣的同一筆記錄在資料庫中存在多個版本,就是上面提到的多版本並發控制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)
這個陣列畫個圖,長這樣:
規則如下:
3 如果在綠色區域,就會有兩種情況:
第三點我在看教程的時候也有點疑惑,好在有熱心網友解答:
落在綠色區域意味著是事務ID 在低水位和高水位這個範圍裡面,而真正是否可見,看綠色區域是否有這個值。如果綠色區域沒有這個交易 ID,則可見,如果有,則不可見。在這個範圍裡面並不代表這個範圍就有這個數值,例如 [1,2,3,5],4 在這個陣列 1-5 的範圍裡,卻沒在這個陣列裡面。
這樣說可能有點難以理解,我假設一個場景:三個事務對同一條資料進行查詢更新等操作,為此畫了張圖以方便理解:
原始資料還是下圖這樣的,對id = 2 的張三進行資訊的更新:
針對上圖,我想提個問題。 分別在 RC(讀取提交)以及 RR(可重複讀取)隔離等級下,T4 和 T5 時間點的查詢 age 值分別是多少呢? T4 更新的值又是多少呢? 思考片刻,相信大家都有自己的答案。答案在文末,希望大家能帶著自己的疑問繼續讀下去。
2.3.2 RR(可重複讀取)下的結果
RR 層級下,查詢只承認在交易啟動前就已經提交完成的數據,一旦啟動事務就會建立視圖。所以使用 start transaction with consistent snapshot 指令,馬上就會建構視圖。
现在假设:
在这种隔离级别下,他们创建视图的时刻如下:
根据上图得,事务 A 的视图数组是[2,3];事务 B 的视图数组是 [2,3,4];事务 C 的视图数组是[2,3,4,5]。分析一波:
T4 时刻,B 读数据都是从当前版本读起,过程是这样的:
T5 时刻,A 读数据都是从当前版本读起,过程是这样的:
这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。
其实视图是否可见主要看创建视图和提交的时机,总结下规律:
事务 B 的 update 语句,如果按照上图的一致性读,好像结果不大对?
如下图周明,B 的视图数组是先生成的,之后事务 C 才提交。那就应该看不见 C 修改的 age = 23 呀?最后 B 怎么得出 24 了?
没错,如果 B 在更新之前执行查询语句,那返回的结果肯定是 age = 22。问题是更新就不能在历史版本更新了呀,否则 C 的更新不就丢失了?
所以,更新有个规则:更新数据都是先读后写(读是更新语句执行,不是我们手动执行),读的就是当前版本的值,叫当前读;而我们普通的查询语句就叫快照读。
因此,在更新时,当前读读到的是 age = 23,更新之后就成 24 啦。
除了更新语句,查询语句如果加锁也是当前读。如果把事务 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;
假设事务 C 不马上提交,但是 age = 23 版本已生成。事务 B 的更新将会怎么走呢?
事务 C 还没提交,写锁还没释放,但是事务 B 的更新必须要当前读且必须加锁。所以事务 B 就阻塞了,必须等到事务 C 提交,释放锁才能继续当前的读。
2.3.3 RC(读提交)下的结果
在读提交隔离级别下,查询只承认在语句启动前就已经提交完成的数据;每一个语句执行之前都会重新算出一个新的视图。
注意:在上图的表格中用于启动事务的是 start transaction with consistent snapshot 命令,它会创建一个持续整个事务的视图。所以,在 RC 级别下,这命令其实不起作用。等效于普通的 start transaction(在执行 sql 语句之前才算是启动了事务)。所以,事务 B 的更新其实是在事务 C 之后的,它还没真正启动事务,而 C 已提交。
现在假设:
在這種隔離層級下,他們建立視圖的時刻如下:
根據上圖得,事務A 的視圖數組是[2,3,4],但它的高水位是6或更大(已創建事務ID 1);事務B 的視圖數組是[ 2,4];事務C 的視圖陣列是[2,5]。分析一波:
T4 時刻,B 讀資料都是從目前版本讀起,過程是這樣的:
mysql影片教學】#
以上是一文詳解MySQL中的事務與 MVCC 原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!