專欄系列文章:MySQL系列專欄mysql
事務(Transaction
)是數據庫系統執行過程當中的一個邏輯處理單元,可由一條簡單的SQL語句組成,也能夠由一組複雜的SQL語句組成。在事務中的操做,要麼都作修改,要麼都不作,這就是事務的目的。web
先準備下面一張帳戶表來供後面測試使用:sql
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`card` varchar(60) NOT NULL COMMENT '卡號',
`balance` int(11) NOT NULL DEFAULT '0' COMMENT '餘額',
PRIMARY KEY (`id`),
UNIQUE KEY `account_u1` (`card`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帳戶表';
複製代碼
標準上,事務必須同時知足四個特性,也就是事務的ACID
特性。正是這些特性,才保證了數據庫事務的安全性。不過數據庫廠商出於各類目的,可能並不會嚴格知足事務的ACID標準。數據庫
在MySQL中,MyISAM 存儲引擎不支持事務,InnoDB 存儲引擎在默認的READ REPEATABLE(RR)
隔離級別下,徹底遵循和知足事務的ACID
特性,因此後續對事務的研究是基於 InnoDB 存儲引擎的。安全
原子性 指一個數據庫事務中的全部操做是不可分割的單元,只有事務中全部的數據庫操做都執行成功,纔算整個事務成功。事務中任何一個SQL語句執行失敗,已經執行成功的SQL語句也必須撤銷,數據庫狀態應該退回到執行事務前的狀態。服務器
好比有下面一個轉帳操做,A 向 B 轉帳 100
:markdown
BEGIN;
UPDATE account SET balance = balance - 100 WHERE card = "A";
UPDATE account SET balance = balance + 100 WHERE card = "B";
COMMIT;
複製代碼
這個轉帳操做就必須是一個原子操做,A 減去 100,B 加上 100,要麼都成功,要麼都回滾,不能有中間狀態,任何一個SQL失敗,都要回滾到執行事務前的狀態。網絡
在咱們看來,就是兩條SQL更新語句,其實在數據庫層面,這兩條SQL語句會涉及不少操做。在前面學習Buffer Pool
的時候,就知道了首先須要將所在的數據頁從磁盤加載到 Buffer Pool,而後更新內存中的頁,再把頁加入Flush鏈表
,而後在某個時刻將髒頁刷盤,其中任何一個步驟失敗(好比數據庫宕機)都須要回滾。多線程
因此原子性是要保證數據庫的整個操做過程都是原子的,其中任何一步失敗都要撤銷。InnoDB底層有一套複雜的機制來保證數據庫操做的原子性,把已經作了的操做恢復成執行以前的樣子,這塊咱們後面會用一篇文章來講明。併發
一致性 指事務將數據庫從一種狀態轉變爲下一種一致的狀態。在事務開始以前和事務結束之後,數據庫的完整性約束沒有被破壞。
例如,account 表中 card 字段是惟一的,無論如何修改這個字段,在事務提交或事務回滾後,card 字段的數據都仍是惟一。若是變得非惟一了,這就破壞了事務的一致性要求。
mysql> INSERT INTO account(card, balance) values ("A", 1000);
1062 - Duplicate entry 'A' for key 'account_u1'
複製代碼
要保證數據庫中數據的一致性,主要有兩個方面:
數據庫自己保證一部分一致性:MySQL數據庫自己能夠創建一些約束,例如爲表創建主鍵、惟一索引、外鍵、聲明某個列爲NOT NULL等。
業務層保證一致性:更多的狀況下,具體業務場景中的約束會比較複雜,並且數據庫創建約束會對數據庫性能有必定損耗。因此每每咱們會在業務代碼層面來對數據作一致性校驗。
隔離性還有其餘的稱呼,如併發控制、可串行化、鎖等。事務的隔離性要求每一個讀寫事務的對象對其餘事務的操做對象能相互分離,即該事務提交前對其餘事務都不可見,一般這使用鎖來實現。
當數據庫上有多個事務同時執行的時候,就可能出現髒讀、不可重複讀、幻讀的問題,這塊咱們後面會再細說。
持久性要求事務一旦提交,其結果就是永久性的。即便發生宕機等故障,數據庫也能將數據恢復。
須要注意的是,持久性是保證事務系統的高可靠性
,而不是高可用性。事務自己能保證結果的永久性,在事務提交後,全部的變化都是永久的。但對於一些外部因素,如磁盤損壞、天然災害等緣由致使數據庫發生問題,那麼全部提交的數據可能都會丟失。對於高可用性的實現,事務自己並不能保證,須要一些系統共同配合來完成。
從事務理論的角度來講,能夠把事務分爲如下幾種類型:
對於InnoDB存儲引擎來講,其支持扁平事務、帶有保存點的事務、鏈事務、分佈式事務。對於嵌套事務,其並不原生支持。
一、扁平事務
扁平事務是事務類型中最簡單的一種,也是使用最爲頻繁的事務。在扁平事務中,全部操做都處於同一層次,由 BEGIN/START TRANSACTION
開始,由 COMMIT
或 ROLLBACK
結束,其間的操做是原子的。
二、帶有保存點的扁平事務
帶有保存點的扁平事務容許在事務執行過程當中回滾到同一事務中較早的一個狀態。咱們能夠在事務過程當中設置一些保存點(Savepoint)
,保存點用來通知系統應該記住事務當前的狀態,以便當以後發生錯誤時,事務能回到保存點當時的狀態。
對於扁平事務來講,其在事務開始的時候隱式地設置了一個保存點,扁平事務就只有這一個保存點,所以,回滾只能回滾到事務開始時的狀態。
能夠經過 SAVEPOINT
建立一個保存點,ROLLBACK TO SAVEPOINT
回滾到某個保存點。
三、鏈事務
鏈事務就是一個事務在提交的時候自動將上下文傳給下一個事務,也就是說一個事務的提交和下一個事務的開始是原子性的,下一個事務能夠看到上一個事務的結果,就好像在一個事務中進行的同樣。
鏈事務可視爲保存點模式的一種變種,不一樣的是,帶有保存點的扁平事務能回滾到任意正確的保存點,而鏈事務中的回滾僅限於當前事務。
MySQL 的鏈式事務能夠經過 SET completion_type = 1
來打開,後面會舉例說明。
四、嵌套事務
嵌套事務是一個層次結構框架,由一個頂層事務控制着各個層次的事務。頂層事務之下嵌套的事務被稱爲子事務,其控制每個局部的變換。子事務提交後不會真的提交,而是等到父事務提交才真正的提交,父事務回滾了,會回滾全部子事務。
MySQL 不支持嵌套事務,不過咱們能夠經過帶有保存點的事務來模擬串行的嵌套事務。
五、分佈式事務
分佈式事務一般是一個在分佈式環境下運行的扁平事務,須要根據數據所在位置訪問網絡中的不一樣節點。後面會有一個專門的系列來學習分佈式事務。
可使用 BEGIN [WORK]
或者 START TRANSACTION;
顯示的開啓一個事務。
在存儲過程當中,MySQL數據庫的分析器會自動將BEGIN
識別爲BEGIN…END
,所以在存儲過程當中只能使用START TRANSACTION
語句來開啓一個事務。
START TRANSACTION
後邊能夠跟隨幾個修飾符:
READ ONLY
:標識當前事務是一個只讀事務,該事務中的數據庫操做只能讀取數據,不能修改數據。READ WRITE
:標識當前事務是一個讀寫事務,該事務中的數據庫操做能夠讀取數據,也能夠修改數據。WITH CONSISTENT SNAPSHOT
:啓動一致性讀。若是不顯式指定事務的訪問模式,該事務的訪問模式默認就是讀寫模式(READ WRITE
)。
例如開啓只讀事務後,就不能修改數據了:
mysql> START TRANSACTION READ ONLY;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE account SET balance = balance - 100 WHERE id = 100;
1792 - Cannot execute statement in a READ ONLY transaction.
複製代碼
可使用 COMMIT [WORK]
來顯示提交事務,WORK
有沒有均可以。
一、自動提交事務
在MySQL命令行的默認設置下,事務都是自動提交的,即執行一條SQL語句後就會自動執行COMMIT
操做。所以要顯式地開啓一個事務需使用命令BEGIN
或START TRANSACTION
,或者執行命令SET autocommit=O
來禁用自動提交。
二、隱式提交事務
使用START TRANSACTION
或BEGIN
開啓了一個事務,或者把系統變量autocommit
的設置爲OFF時,事務就不會進行自動提交。但某些數據庫操做會自動隱式的提交事務,也不須要開始一個事務。
常見的隱式提交事務的語句包括:
CREATE、ALTER、DROP、ALTER
等等。START TRANSACTION
或 BEGIN
開啓事務時,會自動提交上一個事務。ANALYZE TABLE、FLUSH、OPTIMIZE TABLE、REPAIR TABLE
等語句也會隱式提交事務。三、事務提交類型
咱們能夠經過參數completion_type
來控制COMMIT
後的行爲,有三個值:
0/NO_CHAIN
:默認爲NO_CHAIN
,表示 COMMIT 後沒有任何操做。1/CHAIN
:設置爲1
或CHAIN
時,COMMIT
等同於COMMIT AND CHAIN
,表示在事務提交後立刻自動開啓一個相同隔離級別的新事務。2/RELEASE
:設置爲2
或RELEASE
時,COMMIT
等同於COMMIT AND RELEASE
,表示在事務提交後會自動斷開與服務器的鏈接。能夠看到completion_type
默認值爲 NO_CHAIN(0)
:
mysql> SHOW VARIABLES LIKE 'completion_type';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| completion_type | NO_CHAIN |
+-----------------+----------+
複製代碼
completion_type
參數只會影響BEGIN
或START TRANSACTION
開頭,COMMIT
或ROLLBACK
結尾的事務,不會影響自動提交的事務(AUTOCOMMIT=1
)的事務。
completion_type
設置爲1
時此時就變成了前面說的鏈事務。例以下面的操做,COMMIT
以後立馬開啓了一個新的事務,因此"B"這條數據才能夠被回滾。設置爲0
的狀況下是不會回滾的。最後查詢就只插入了"A"這條數據。
若是要開啓鏈事務,能夠直接使用 COMMIT [WORK] AND CHAIN;
來實現,而無需設置 completion_type=1
。
mysql> TRUNCATE account;
Query OK, 0 rows affected (0.01 sec)
mysql> SET completion_type = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES ("A");
Query OK, 1 row affected (0.01 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO account(card) VALUES ("B");
Query OK, 1 row affected (0.00 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT * FROM account;
+----+------+---------+
| id | card | balance |
+----+------+---------+
| 1 | A | 0 |
+----+------+---------+
複製代碼
completion_type
設置爲2
時此時 COMMIT
以後就會斷開鏈接,再操做就會報鏈接斷開的錯誤,有些客戶端會自動嘗試從新鏈接。
mysql> SET completion_type = 2;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES ("C");
Query OK, 1 row affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM account WHERE card = "C";
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 3
Current database: test
+----+------+---------+
| id | card | balance |
+----+------+---------+
| 1 | C | 0 |
+----+------+---------+
1 row in set (0.00 sec)
複製代碼
可使用 ROLLBACK [WORK]
來終止事務,撤銷正在進行的未提交的修改。
要注意的是,ROLLBACK
語句是手動的回滾事務時纔去使用的,若是事務在執行過程當中遇到了某些錯誤而沒法繼續執行的話,事務自身會自動的回滾。
保存點的操做方法以下:
SAVEPOINT <identifier>
RELEASE SAVEPOINT <identifier>
ROLLBACK TO [SAVEPOINT] <identifier>
須要注意的是,ROLLBACK TO SAVEPOINT
,只是回滾到了指定的保存點,其並非真正地結束一個事務,最後還須要顯式地運行COMMIT
或ROLLBACK
命令。
例以下面的操做,
mysql> TRUNCATE account;
Query OK, 0 rows affected (0.01 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES("A");
Query OK, 1 row affected (0.00 sec)
mysql> SAVEPOINT P1;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES("B");
Query OK, 1 row affected (0.00 sec)
mysql> ROLLBACK TO P1;
Query OK, 0 rows affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM account;
+----+------+---------+
| id | card | balance |
+----+------+---------+
| 1 | A | 0 |
+----+------+---------+
1 row in set (0.00 sec)
複製代碼
咱們的業務系統每每都是多線程併發執行多個事務,數據庫層面也會多個事務併發執行,那麼就可能會對同一條數據查詢和修改。既然是併發,跟Java中的多線程同樣,就會有線程安全問題。
併發事務涉及到四個問題:髒寫、髒讀、不可重複讀、幻讀。按問題的嚴重程度排個序就是:髒寫 > 髒讀 > 不可重複讀 > 幻讀
。
若是一個事務A讀到了另外一個事務B修改過的未提交的數據,那事務A的讀取就是髒讀,由於事務A讀取的數據是非持久性的數據。
例如按下面的時間線執行:事務B更新了數據,事務A讀取了事務B未提交的數據,可是在t5時刻事務B回滾了這次操做,事務A查詢到的數據就是髒數據,若是繼續用這個髒數據作業務就會有問題。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查詢餘額爲100; | 查詢餘額爲100; |
t3 | 餘額增長100; | |
t4 | 查詢約爲200; | |
t5 | ROLLBACK; | |
t6 | COMMIT; | |
t7 | A事務讀取到B事務未提交的數據,致使讀取到的是髒數據 |
在沒有髒讀的狀況下,若是一個事務屢次讀取同一個數據不一致,那說明發生了不可重複讀的問題,也就是同一個數據沒法重複讀取,違反了數據庫事務一致性的要求。
例如按下面的時間線執行:在事務A中,第一次查詢爲100,此時事務B修改了餘額而且提交了事務,事務A再次查詢就讀取到事務B已提交的數據,在同一個事務中,兩次查詢的結果不一致,就是不可重複讀取。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查詢餘額爲100; | 查詢餘額爲100; |
t3 | 餘額增長100; | |
t4 | COMMIT; | |
t5 | 查詢餘額爲200; | |
t6 | COMMIT; | |
t7 | A事務讀取到B事務已提交的數據,屢次讀取同一條數據不一致 |
其實不可重複讀在一些場景下也能夠認爲不是問題,好比我就但願在一個事務中,別的事務修改了數據,我立馬也能讀到,那就是不可重複讀的。若是但願在一個事務中屢次讀取是同樣的,就是可重複讀。
幻讀就是一個事務用一樣的條件查詢,因爲另外一個事務新增了數據,致使看到了以前沒有的數據。
例如按下面的時間線執行,事務A將全部帳戶餘額都改成100了,而後事務B新增了一個帳戶,結果事務A再次查詢發現還有一個帳戶的餘額爲0,以前更新的數據中是沒有這條記錄的,這就是幻讀。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 更新全部帳戶餘額爲100; | |
t3 | 新增一個帳戶,餘額爲0; | |
t4 | COMMIT; | |
t5 | 查詢發現還有一個帳戶餘額爲0; | |
t6 | COMMIT; | |
t7 | 因爲B事務新增數據,致使A事務操做以後還有以前沒看到過的數據 |
髒寫也稱爲數據丟失、更新丟失,簡單來講就是一個事務的更新操做會被另外一個事務的更新操做所覆蓋,從而致使數據的不一致。
有兩類狀況會致使髒寫:
一、事務A回滾把事務B已提交的修改給覆蓋了,就會形成事務B的修改丟失。
例如按下面的時間線執行,事務A、B開始時查詢餘額都爲0,事務B修改餘額增長了200,事務A回滾將餘額變爲0,事務B看起來就是修改的數據丟失了。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查詢餘額爲0; | 查詢餘額爲0; |
t3 | 餘額增長100; | |
t4 | 餘額增長200; | |
t5 | COMMIT; | |
t6 | ROLLBACK; | |
t7 | 查詢餘額爲0; | 查詢餘額爲0; |
t8 | 因爲事務A回滾,致使事務B更新的數據沒了 |
不過InnoDB存儲引擎不會發生這個問題,由於InnoDB在更新數據時加了排他鎖, 這樣在事務A在未完成的時候, 其餘事務是沒法對事務A涉及到的數據作修改並提交的。例如上面的示意圖中,事務B在執行餘額增長200的時候,因爲事務A修改了同一條數據且未提交,這時這條數據已經加了排它鎖了,所以事務B修改時會阻塞住,等待加鎖後才能修改。
二、事務A覆蓋了事務B已提交的修改,形成事務B的修改丟失。
例如按下面的時間線執行,事務A、B一開始查詢餘額都爲0,事務B先增長了200,並提交了事務。接着事務A在餘額爲0的基礎上增長100,而後提交事務。最後就是餘額只有100,事務B的修改丟失了。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查詢餘額爲0; | 查詢餘額爲0; |
t3 | 餘額增長200; | |
t4 | COMMIT; | |
t5 | 餘額增長100; | |
t6 | COMMIT; | |
t7 | 查詢餘額爲100; | 查詢餘額爲100; |
t8 | 事務A將事務B提交的修改覆蓋掉,致使事務B的修改丟失 |
這種狀況有兩種方式能夠避免髒寫發生:
一種是基於數據庫悲觀鎖,在查詢時使用 for update
實現一個排它鎖,保證在該事務結束時其餘事務沒法更新該數據。不過這樣就會致使併發更新的性能下降。
SELECT * FROM account WHERE id = 1 FOR UPDATE;
複製代碼
另外一種是基於樂觀鎖,能夠在表中增長一個版本號字段,查詢時將版本號查出來,更新時帶上版本號做爲條件,更新成功則是同一條記錄,不然就時更新失敗。更新失敗就能夠返回「記錄不存在或版本不一致」這樣的錯誤,讓用戶能夠從新查詢再更新一次。
UPDATE account SET balance=balance+100, version=version+1 where id = 1 and version = 1
複製代碼
前面說到併發事務有四個問題:髒寫、髒讀、不可重複度、幻讀。其中,髒寫能夠經過樂觀鎖或悲觀鎖的方式來解決,剩下的3個問題,實際上是數據庫讀一致性
形成的,須要數據庫提供必定的事務隔離機制來解決,也就是事務的隔離性
。
SQL標準定義了四個隔離級別:
READ UNCOMMITTED
:讀未提交,簡稱 RU。READ COMMITTED
:讀已提交,簡稱 RC。REPEATABLE READ
:可重複讀,簡稱 RR。SERIALIZABLE
:可串行化。不一樣的隔離級別,分別能解決一部分事務問題,具體狀況可查看下面的表格。
READ UNCOMMITTED
:會發生髒讀、不可重複讀、幻讀的問題。READ COMMITTED
:會發生不可重複讀、幻讀的問題,不會發生髒讀的問題。REPEATABLE READ
:會發生幻讀的問題,不會發生髒讀、不可重複讀的問題。SERIALIZABLE
:髒讀、不可重複讀、幻讀的問題都不會發生。隔離級別越低,可能產生的問題越嚴重;隔離級別越高,併發的性能也會越低。不過不多有數據庫廠商遵循這些SQL標準,好比Oracle數據庫就不支持READ UNCOMMITTED
和REPEATABLE READ
的事務隔離級別。
InnoDB存儲引擎支持SQL標準的四種隔離級別,不過InnoDB在REPEATABLE READ
隔離級別下就能避免幻讀問題的發生。
MySQL的默認隔離級別爲REPEATABLE READ
,能夠經過下面的語句修改事務的隔離級別:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL <level>;
level 可選值:
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
複製代碼
若是想在服務器啓動時修改事務的默認隔離級別,能夠在[mysqld]
下添加參數transaction-isolation
。
[mysqld]
transaction-isolation = READ-COMMITTED
複製代碼
查看當前會話的事務隔離級別:
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
複製代碼
查看全局的事務隔離級別:
mysql> SELECT @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
複製代碼
數據庫底層有一套複雜的機制來實現事務的ACID
特性,這節作個簡單說明,接下來會用幾篇單獨的文章來介紹。
一、持久性(D)
事務的持久性經過數據庫的redo log
來實現,redo log
稱爲重作日誌。在更新Buffer Pool
中的數據頁時,會同時記錄對應的 redo log,這樣就算髒頁沒有刷盤,在MySQL宕機重啓時,也能夠經過 redo log 來恢復數據。
二、原子性(A)
事務的原子性經過數據庫的undo log
來實現,undo log
稱爲撤銷日誌或回滾日誌。在一個事務中進行增刪改操做時,都會記錄對應的 undo log。
並且 undo log 造成的版本鏈還用於實現多版本併發控制(MVCC)
,InnoDB的RC
和RR
隔離級別就是是基於MVCC
來實現高性能事務,並且經過MVCC
來避免幻讀的發生。
三、隔離性(I)
事務的隔離性由鎖
來實現,不一樣的加鎖方式,能夠實現不一樣的事務隔離機制。
四、一致性(C)
事務的一致性須要兩個層面來保證:
AID
三大特性,纔有可能實現一致性。例如,原子性沒法保證,顯然一致性也沒法保證。能夠看到,原子性、持久性、隔離性是數據庫層面保證持久性的手段。所以,咱們後面會分別針對原子性、持久性、隔離性單獨用一篇文章來學習。