提到事務首先想到的固然是事務的四個特性:原子性、一致性、隔離性、持久性。事務的實現是由引擎層面來實現的,所以不一樣的存儲引擎可能對事務有不一樣的實現方案。好比 MySQL 的 MyISAM 引擎就沒有實現事務,這也是其被 InnoDB 所替代的緣由之一。html
原子性: 事務的全部操做在數據庫中要麼所有正確的反映出來,要麼徹底不反映。mysql
一致性: 事務執行先後數據庫的數據保持一致。git
隔離性: 多個事務併發執行時,對於任何一對事務Ti和Tj,在Ti看來,Tj 要麼在 Ti 以前已經完成執行,或者在Ti完成以後開始執行。所以,每一個事務都感受不到系統中有其餘事務在併發執行。github
持久性: 一個事務成功完成後,它對數據庫的改變必須是永久的,即便事務剛提交機器就宕機了數據也不能丟。sql
事務的原子性和持久性比較好理解,可是一致性會更加抽象一些。對於一致性常常有個轉帳的例子,A 給 B 轉帳,轉帳先後 A 和 B 的帳戶總和不變就是一致的。這個例子咋一看好像很清楚,但轉念一想原子性是否是也能達到這個目的呢?答案是:不能,原子性能夠保證 A 帳戶扣減和 B 帳戶增長同時成功或者同時失敗,可是並不能保證 A 扣減的數量等於 B 增長的數量。其實是爲了達到一致性因此要同時知足其餘三個條件。數據庫
還有一個事務的隔離性比較複雜,由於 MySQL 的事務能夠有多種隔離級別,接下里一塊兒看看。數組
當多個事務併發執行時可能存在髒讀(dirty read),不可重複讀(non-repeatable read)和幻讀(phantom read),爲了解決這些問題所以引入了不一樣的隔離級別。bash
髒讀: 事務 A 和事務 B 併發執行時,事務 B 能夠讀到事務 A 未提交的數據,就發生了髒讀。髒讀的本質在於事務 B 讀了事務 A 未提交的數據,若是事務 A 發生了回滾,那麼事務 B 讀到的數據其實是無效的。以下面案例所示:事務 B 查詢到 value 的結果爲100,可是由於事務 A 發生了回滾,所以 value 的值不必定是 100。markdown
事務 A | 事務 B |
---|---|
begin | begin |
update t set value = 100 | |
select value from t | |
rollback | |
commit | commit |
不可重複讀: 在一個事務中,屢次查詢同一個數據會獲得不一樣的結果,就叫不可重複讀。以下面案例所示:事務 B 兩次查詢 value 的結果不一致。session
事務 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稱爲高水位。當某個事務讀取某條數據時,從該數據的最高版本開始,若是讀得起那麼就取這個數據,若是讀不起就取更低一個版本的數據,如此循環,直到能讀取有效數據。
在判斷讀得起和讀不起時就只有如下幾種狀況:
數據版本號大於等於事務的高水位,說明是後面的事務建立的,讀不起;
數據版本號小於等於低水位,說明是事務開啓前就已經提交的,或者是本事務本身修改的,讀得起;
數據版本號介於高水位和低水位之間,若是該版本號在數組裏,說明是未提交的,讀不起。
數據版本號介於高水位和低水位之間,若是該版本號不在數組裏,說明是已經提交的,讀的起。
提交讀和可重複讀的區別在於,提交讀每次執行語句前更新這個數組,這樣已經提交的數據就不在數組裏,就會被看到,可重複讀就是始終使用事務開啓時生成的數組。
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版)