1. 程式人生 > 程式設計 >MySQL的事務

MySQL的事務

MySQL的事務

提到事務首先想到的當然是事務的四個特性:原子性、一致性、隔離性、永續性。事務的實現是由引擎層面來實現的,因此不同的儲存引擎可能對事務有不同的實現方案。比如 MySQL 的 MyISAM 引擎就沒有實現事務,這也是其被 InnoDB 所替代的原因之一。

事務的四個特性

原子性: 事務的所有操作在資料庫中要麼全部正確的反映出來,要麼完全不反映。

一致性: 事務執行前後資料庫的資料保持一致。

隔離性: 多個事務併發執行時,對於任何一對事務Ti和Tj,在Ti看來,Tj 要麼在 Ti 之前已經完成執行,或者在Ti完成之後開始執行。因此,每個事務都感覺不到系統中有其他事務在併發執行。

永續性:

一個事務成功完成後,它對資料庫的改變必須是永久的,即使事務剛提交機器就宕機了資料也不能丟。

事務的原子性和永續性比較好理解,但是一致性會更加抽象一些。對於一致性經常有個轉賬的例子,A 給 B 轉賬,轉賬前後 A 和 B 的賬戶總和不變就是一致的。這個例子咋一看好像很清楚,但轉念一想原子性是不是也能達到這個目的呢?答案是:不能,原子性可以保證 A 賬戶扣減和 B 賬戶增加同時成功或者同時失敗,但是並不能保證 A 扣減的數量等於 B 增加的數量。實際上是為了達到一致性所以要同時滿足其他三個條件。

還有一個事務的隔離性比較複雜,因為 MySQL 的事務可以有多種隔離級別,接下里一起看看。

事務的隔離級別

當多個事務併發執行時可能存在髒讀(dirty read),不可重複讀(non-repeatable read)和幻讀(phantom read),為瞭解決這些問題因此引入了不同的隔離級別。

髒讀: 事務 A 和事務 B 併發執行時,事務 B 可以讀到事務 A 未提交的資料,就發生了髒讀。髒讀的本質在於事務 B 讀了事務 A 未提交的資料,如果事務 A 發生了回滾,那麼事務 B 讀到的資料實際上是無效的。如下面案例所示:事務 B 查詢到 value 的結果為100,但是因為事務 A 發生了回滾,因此 value 的值不一定是 100。

事務 A 事務 B
begin begin
update t set value = 100
select value from t
rollback
commit commit

不可重複讀: 在一個事務中,多次查詢同一個資料會得到不同的結果,就叫不可重複讀。如下面案例所示:事務 B 兩次查詢 value 的結果不一致。

事務 A 事務 B
begin begin
update t set value = 100
select value from t ( value = 100 )
update t set value = 200
select value from t ( value = 200 )
commit commit

幻讀: 在一個事務中進行範圍查詢,查詢到了一定條數的資料,但是這個時候又有新的資料插入就導致資料庫中資料多了一行,這就是幻讀。如下面案例所示:事務 B 兩次查詢到的資料行數不一樣。

事務 A 事務 B
begin begin
select * from t
insert into t ...
commit
select * from t
commit

MySQL 的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)可重複讀(repeatable read)和序列化(serializable)。

未提交讀: 一個事務還未提交,其造成的更新就可以被其他事務看到。這就造成了髒讀。

讀提交: 一個事務提交後,其更改才能被其他事務所看到。讀提交解決了髒讀的問題。

可重複讀: 在一個事務中,多次讀取同一個資料得到的結果總是相同的,即使有其他事務更新了這個資料並提交成功了。可重複讀解決了不可重複讀的問題。但是還是會出現幻讀。InnoDB 引擎通過多版本併發控制(Multiversion concurrency control,MVCC)解決了幻讀的問題。

序列化: 序列話是最嚴格的隔離級別,在事務中對讀操作加讀鎖,對寫操作加寫鎖,所以可能會出現大量鎖爭用的場景。

從上到下,隔離級別越來越高,效率相應也會隨之降低,對於不同的隔離級別需要根據業務場景進行合理選擇。

查詢和修改事務的隔離級別

下面的命令可以查詢 InnoDB 引擎全域性的隔離級別和當前會話的隔離級別

mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+
複製程式碼

設定innodb的事務級別方法是:

set 作用域 transaction isolation level 事務隔離級別

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

mysql> set global transaction isolation level read committed; // 設定全域性的隔離級別為讀提交

mysql> set session transaction isolation level read committed; // 設定當前會話的隔離級別為讀提交
複製程式碼

事務的啟動方式

  • MySQL 裡可以通過 begin 命令或 start transaction 來顯示啟動一個事務。顯示開啟的事務,需要使用 commit 命令進行提交。

  • MySQL 裡如果沒有顯示執行命令開啟事務,MySQL 也會在執行第一條命令的時候自動開啟事務。如果自動提交 autocommit 處於開啟狀態,那麼自動開啟的事務也會被自動提交。那麼執行一條 select 語句時,MySQL 首先會自動開啟一個事務,並且在 select 語句執行完後自動提交。因此,在 MySQL 裡執行一條語句時也是一個完整的事務。

  • 在 MySQL 裡執行命令 set autocommit=0 可以關閉事務的自動提交。如果 autocommit 處於關閉狀態,那麼執行一條 select 語句時仍然會開啟一個事務,並且在執行完成後不會自動提交。

  • begin 和 start transaction 命令並不是執行後立即開啟一個事務,而是在執行第一條語句時才開啟事務。start transaction with consistent snapshot 命令才是執行後就立即開啟事務。

舉例說明不同隔離級別的影響

接下來我們用一個案例來看不同隔離級別下會有怎樣不同的結果。

create table t (k int) ENGINE=InnoDB;
insert into t values (1);
複製程式碼
事務 A 事務 B
begin
1: select k from t
begin; update t set k = k + 1
2: select k from t
commit
3: select k from t
commit
4: select k from t

隔離級別為未提交讀時:對於事務 A,第1條查詢語句的結果是1,第2條查詢語句的結果是2,第3條和第4條查詢語句的結果也都是2。

隔離級別為讀提交時:對於事務 A,第1條查詢語句的結果是1,第2條查詢語句的結果是1,第3條查詢語句的結果是2,第4條查詢語句的結果也是2。

隔離級別為可重複讀時:對於事務 A,第1條、第2條和第3條查詢語句的結果都是1,第4條查詢語句的結果是2。

隔離級別為序列化時:對於事務 A,第1條查詢語句的結果是1。這時事務 B 執行更新語句時會被阻塞,因為事務 A 在這條資料上加上了讀鎖,事務 B 要更新這個資料就必須加寫鎖,由於讀鎖和寫鎖衝突,因此事務 B 只能等到事務 A 提交後釋放讀鎖才能進行更新。因此,事務 A 的第2條和第3條查詢語句的結果也是1,第4條查詢語句的結果是2。

事務隔離性的實現

事務的隔離性通過 undo log 日誌來實現,對於同一條資料,InnoDB 會儲存其多個版本,多個版本則是通過 undo log 日誌來實現,將當前值回滾不同的次數就可以得到不同低版本的資料,這就是資料庫的多版本併發控制(MVCC)。當然只有 undo log 日誌還不行,為了支援提交讀和可重複讀兩種隔離級別,一個事務 Ti 如何知道自己應該使用哪個版本的資料呢?InnoDB 的做法是維護一個一致性檢視來現實。

InnoDB 給每一個事務維護一個唯一的事務 ID,事務 ID 是嚴格遞增分配的,也就是後開啟的事務的事務 ID 一定比先開啟的事務的事務 ID 要大。因為通過 undo log 日誌可以得到多個版本的資料,可以假想在資料庫中每個資料有多個版本。每個事務更新一個資料時,就會生成一個新版本資料並且將自己的事務 ID 貼在這個版本的資料上,用來標識這個資料的版本。

當開啟一個新的事務時,InnoDB 會為每一個事務維護一個陣列,這個陣列中儲存了當前活躍的事務的事務ID,所謂活躍的事務指事務已經開始,但是還未提交的事務。在這個陣列中最小的事務 ID 將其稱為低水位,最大的事務 ID 加1稱為高水位。當某個事務讀取某條資料時,從該資料的最高版本開始,如果讀得起那麼就取這個資料,如果讀不起就取更低一個版本的資料,如此迴圈,直到能讀取有效資料。

在判斷讀得起和讀不起時就只有以下幾種情況:

  1. 資料版本號大於等於事務的高水位,說明是後面的事務建立的,讀不起;

  2. 資料版本號小於等於低水位,說明是事務開啟前就已經提交的,或者是本事務自己修改的,讀得起;

  3. 資料版本號介於高水位和低水位之間,如果該版本號在陣列裡,說明是未提交的,讀不起。

  4. 資料版本號介於高水位和低水位之間,如果該版本號不在陣列裡,說明是已經提交的,讀的起。

提交讀和可重複讀的區別在於,提交讀每次執行語句前更新這個陣列,這樣已經提交的資料就不在陣列裡,就會被看到,可重複讀就是始終使用事務開啟時生成的陣列。

快照讀和當前讀

InnoDB 給每一個事務生成一個唯一事務 ID 的方法稱為生成快照,因此這種場景稱為快照讀。但是對於更新資料不能使用快照讀,因為更新資料時如果使用快照讀會可能會覆蓋其他事務的更改。另外查詢時如果加鎖也會採用當前讀的方式。當前讀就是讀這個資料最新的提交資料。InnoDB 的多版本併發控制實現了在序列化的隔離級別下讀不加鎖,提高了併發效能。

下面通過一個例子來理解快照讀和當前讀:

首先建一個表 t,並插入一條資料。

mysql-> create table t(k int)ENGINE=InnoDB;
mysql-> insert into t(k) values (1);
複製程式碼

然後將事務的隔離級別設定為 REPEATABLE-READ,接著開啟三個事務,並按照下面的順序進行執行。

事務 A 事務 B 事務 C
start transaction with consistent snapshot
start transaction with consistent snapshot
select k fromt t;
select k from t;
update t set k = k + 1;
update t set k = k + 1;
select k from t; commit;
select k from t; commit;

結果是:事務 A 兩次讀取的結果都是1,事務 B 第一次讀取的結果是1,第二次讀取的結果是 3。事務 A 兩次都是快照讀,在可重複讀的隔離級別下,因此兩次讀到的結果相同。事務 B 第一次是快照讀,但是 update 語句進行了一次當前讀將 k 的值更新為事務 C 已經提交的結果 2,並且在此基礎上再加1得到3。執行了 update 操作時會建立一個新版本的資料,並且將自己的事務 ID 作為該資料的版本號,因此在該事務內可以讀到自己更新的資料。因此事務 B 最後一次查詢的結果是 3。

最近在學習 MySQL 的原理,一篇文章做個筆記。

參考

[1] 資料庫系統概念(第6版)

[2] MySQL實戰45講,林曉斌

[3] 高效能MySQL(第3版)

[4] 事務的隔離級別和mysql事務隔離級別修改

[5] MySQL 加鎖處理分析,何登成