MySQL/MariaDB中的事務和事務隔離級別


官方手冊:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-transaction-model.html

1.事務特性

事務具有ACID特性:原子性(A,atomicity)、一致性(C,consistency)、隔離性(I,isolation)、持久性(D,durabulity)。

  • 原子性:事務內的所有操作要么都執行,要么都不執行。
  • 一致性:事務開始和結束前后,數據都滿足數據一致性約束,而不是經過事務控制之后數據變得不滿足條件或業務規則。
  • 隔離性:事務之間不能互影響,它們必須完全的各行其道,互不可見。
  • 持久性:事務完成后,該事務內涉及的數據必須持久性的寫入磁盤保證其持久性。當然,這是從事務的角度來考慮的的持久性,從操作系統故障或硬件故障來說,這是不一定的。

2.事務分類

  • 扁平事務
  • 帶保存點的扁平事務
  • 鏈事務
  • 嵌套事務
  • 分布式事務

2.1 扁平事務

即最常見的事務。由begin開始,commit或rollback結束,中間的所有操作要么都回滾要么都提交。扁平事務在生產環境中占絕大多數使用情況。因此每一種數據庫產品都支持扁平事務。

扁平事務的缺點在於無法回滾或提交一部分,只能全部回滾或全部提交,所以就有了"帶有保存點"的扁平事務。

2.2 帶有保存點的扁平事務

通過在事務內部的某個位置使用savepoint,將來可以在事務中回滾到此位置。

MariaDB/MySQL中設置保存點的命令為:

savepoint [savepoint_name]

回滾到指定保存點的命令為:

rollback to savepoint_name

刪除一個保存點的命令為:

release savepoint savepoint_name

實際上,扁平事務也是有保存點的,只不過它只有一個隱式的保存點,且自動建立在事務開始的位置因此扁平事務只能回滾到事務開始處。

2.3 鏈式事務

鏈式事務是保存點扁平事務的變種。它在一個事務提交的時候自動隱式的將上下文傳給下一個事務,也就是說一個事務的提交和下一個事務的開始是原子性的,下一個事務可以看到上一個事務的處理結果。通俗地說,就是事務的提交和事務的開始是鏈接式下去的。

這樣的事務類型,在提交事務的時候,會釋放要提交事務內所有的鎖和要提交事務內所有的保存點。因此鏈式事務只能回滾到當前所在事務的保存點,而不能回滾到已提交的事務中的保存點。

2.4 嵌套事務

嵌套事務由一個頂層事務控制所有的子事務。子事務的提交完成后不會真的提交,而是等到頂層事務提交才真正的提交。

關於嵌套事務的機制,主要有以下3個結論:

  • 回滾內部事務的同時會回滾到外部事務的起始點。
  • 事務提交時從內向外依次提交。
  • 回滾外部事務的同時會回滾所有事務,包括已提交的內部事務。因為只提交內部事務時沒有真的提交。

不管怎么樣,最好少用嵌套事務。且MariaDB/MySQL不原生態支持嵌套事務(SQL Server支持)。

2.5 分布式事務

將多個服務器上的事務(節點)組合形成一個遵循事務特性(acid)的分布式事務。

例如在工行atm機轉賬到建行用戶。工行atm機所在數據庫是一個事務節點A,建行數據庫是一個事務節點B,僅靠工行atm機是無法完成轉賬工作的,因為它控制不了建行的事務。所以它們組成一個分布式事務:

  • 1.atm機發出轉賬口令。
  • 2.atm機從工行用戶減少N元。
  • 3.在建行用戶增加N元。
  • 4.在atm機上返回轉賬成功或失敗。

上面涉及了兩個事務節點,這些事務節點之間的事務必須同時具有acid屬性,要么所有的事務都成功,要么所有的事務都失敗,不能只成功atm機的事務,而建行的事務失敗。

MariaDB/MySQL的分布式事務使用兩段式提交協議(2-phase commit,2PC)。最重要的是,MySQL 5.7.7之前,MySQL對分布式事務的支持一直都不完善(第一階段提交后不會寫binlog,導致宕機丟失日志),這個問題持續時間長達數十年,直到MySQL 5.7.7,才完美支持分布式事務。相關內容可參考網上一篇文章:https://www.linuxidc.com/Linux/2016-02/128053.htm。遺憾的是,MariaDB至今(MariaDB 10.3.6)都沒有解決這個問題。

3.事務控制語句

  • begin 和 start transaction表示顯式開啟一個事務。它們之間並沒有什么區別,但是在存儲過程中,begin會被識別成begin...end的語句塊,所以存儲過程只能使用start transaction來顯式開啟一個事務。
  • commit 和 commit work用於提交一個事務。
  • rollback 和 rollback work用於回滾一個事務。
  • savepoint identifier表示在事務中創建一個保存點。一個事務中允許存在多個保存點。
  • release savepoint identifier表示刪除一個保存點。當要刪除的保存點不存在的時候會拋出異常。
  • rollback to savepoint表示回滾到指定的保存點,回滾到保存點后,該保存點之后的所有操縱都被回滾。注意,rollback to不會結束事務,只是回到某一個保存點的狀態。
  • set transaction用來設置事務的隔離級別。可設置的隔離級別有read uncommitted/read committed/repeatable read/serializable。

commit與commit work以及rollback與rollback work作用是一樣的。但是他們的作用卻和變量completion_type的值有關。

例如將completion_type設置為1,進行測試。

mysql> set completion_type=1;
mysql> begin;
mysql> insert into ttt values(1000);
mysql> commit work;
mysql> insert into ttt values(2000);
mysql> rollback;
mysql> select * from ttt where id>=1000;
+------+
| id   |
+------+
| 1000 |
+------+
1 row in set (0.00 sec)

begin開始事務后,插入了值為1000的記錄,commit work了一次,然后再插入了值為2000的記錄后rollback,查詢結果結果中只顯示了1000,而沒有2000,因為commit work提交后自動又開啟了一個事務,使用rollback會回滾該事務。

將completion_type設置為2,進行測試。

mysql> set completion_type=2;
mysql> begin;
mysql> insert into ttt select 1000;
mysql> commit;

提交后,再查詢或者進行其他操作,結果提示已經和MariaDB/MySQL服務器斷開連接了。

mysql> select * from ttt;
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...

4.顯式事務的次數統計

通過全局狀態變量com_commitcom_rollback可以查看當前已經顯式提交和顯式回滾事務的次數。還可以看到回滾到保存點的次數。

mysql> show global status like "%com_commit%";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_commit    | 14    |
+---------------+-------+
mysql> show global status like "%com_rollback%";
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| Com_rollback              | 24    |
| Com_rollback_to_savepoint | 0     |
+---------------------------+-------+

5.一致性非鎖定讀(快照查詢)

在innodb存儲引擎中,存在一種數據查詢方式:快照查詢。因為查詢的是快照數據,所以查詢時不申請共享鎖。

當進行一致性非鎖定讀查詢的時候,查詢操作不會去等待記錄上的獨占鎖釋放,而是直接去讀取快照數據。快照數據是通過undo段來實現的,因此它基本不會產生開銷。顯然,通過這種方式,可以極大的提高讀並發性。

快照數據其實是行版本數據,一個行記錄可能會存在多個行版本,並發時這種讀取行版本的方式稱為多版本並發控制(MVCC)。在隔離級別為read committed和repeatable read時,采取的查詢方式就是一致性非鎖定讀方式。但是,不同的隔離級別下,讀取行版本的方式是不一樣的。在后面介紹對應的隔離級別時會作出說明。

下面是在innodb默認的隔離級別是repeatable read下的實驗,該隔離級別下,事務總是在開啟的時候獲取最新的行版本,並一直持有該版本直到事務結束。更多的"一致性非鎖定讀"見后文說明read committed和repeatable read部分。

當前示例表ttt的記錄如下:

mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

在會話1執行:

mysql> begin;
mysql> update ttt set id=100 where id=1

在會話2中執行:

mysql> begin;
mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

查詢的結果和預期的一樣,來自開啟事務前最新提交的行版本數據。

回到會話1提交事務:

mysql> commit;

再回到會話2中查詢:

mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

再次去會話1更新該記錄:

mysql> begin;
mysql> update ttt set id=1000 where id=100;
mysql> commit;

再回到會話2執行查詢:

mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

這就是repeatable read隔離級別下的一致性非鎖定讀的特性。

當然,MySQL也支持一致性鎖定讀的方式。

6.一致性鎖定讀

在隔離級別為read committed和repeatable read時,采取的查詢方式就是一致性非鎖定讀方式。但是在某些情況下,需要人為的對讀操作進行加鎖。MySQL中對這種方式的支持是通過在select語句后加上lock in share mode或者for update

  • select ... from ... where ... lock in share mode;
  • select ...from ... where ... for update;

使用lock in share mode會對select語句要查詢的記錄加上一個共享鎖(S),使用for update語句會對select語句要查詢的記錄加上獨占鎖(X)。

另外,對於一致性非鎖定讀操作,即使要查詢的記錄已經被for update加上了獨占鎖,也一樣可以讀取,就和純粹的update加的鎖一樣,只不過此時讀取的是快照數據而已。

7.事務隔離級別

SQL標准定義了4中隔離級別:read uncommitted、read committed、repeatable read、serializable。

MariaDB/MySQL也支持這4種隔離級別。但是要注意的是,MySQL中實現的隔離級別和SQL Server實現的隔離級別在同級別上有些差別。在后面有必要說明地方會給出它們的差異之處。

MariaDB/MySQL中默認的隔離級別是repeatable read,SQL Server和oracle的默認隔離級別都是read committed。

事務特性(ACID)中的隔離性(I,isolation)就是隔離級別,它通過鎖來實現。也就是說,設置不同的隔離級別,其本質只是控制不同的鎖行為。例如操作是否申請鎖,什么時候申請鎖,申請的鎖是立刻釋放還是持久持有直到事務結束才釋放等。

7.1 設置和查看事務隔離級別

隔離級別是基於會話設置的,當然也可以基於全局進行設置,設置為全局時,不會影響當前會話的級別。設置的方法是:

set [global | session] transaction isolation level {type} type: read uncommitted | read committed | repeatable read | serializable

或者直接修改變量值也可以:

set @@global.tx_isolation = 'read-uncommitted' | 'read-committed' | 'repeatable-read' | 'serializable' set @@session.tx_isolation = 'read-uncommitted' | 'read-committed' | 'repeatable-read' | 'serializable'

查看當前會話的隔離級別方法如下:

mysql> select @@tx_isolation;
mysql> select @@global.tx_isolation;
mysql> select @@tx_isolation;select @@global.tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ       |
+-----------------------+

注意,事務隔離級別的設置只需在需要的一端設置,不用在兩邊會話都設置。例如想要讓會話2的查詢加鎖,則只需在會話2上設置serializable,在會話1設置的serializable對會話2是沒有影響的,這和SQL Server中一樣。但是,MariaDB/MySQL除了serializable隔離級別,其他的隔離級別都默認會讀取舊的行版本,所以查詢永遠不會造成阻塞。而SQL Server中只有基於快照的兩種隔離級別才會讀取行版本,所以在4種標准的隔離級別下,如果查詢加的S鎖被阻塞,查詢會進入鎖等待。

在MariaDB/MySQL中不會出現更新丟失的問題,因為獨占鎖一直持有直到事務結束。當1個會話開啟事務A修改某記錄,另一個會話也開啟事務B修改該記錄,該修改被阻塞,當事務A提交后,事務B中的更新立刻執行成功,但是執行成功后查詢卻發現數據並沒有隨着事務B的想法而改變,因為這時候事務B更新的那條記錄已經不是原來的記錄了。但是事務A回滾的話,事務B是可以正常更新的,但這沒有丟失更新。

7.2 read uncommitted

該級別稱為未提交讀,即允許讀取未提交的數據。

在該隔離級別下,讀數據的時候不會申請讀鎖,所以也不會出現查詢被阻塞的情況。

在會話1執行:

create table ttt(id int);
insert into ttt select 1;
insert into ttt select 2;
begin;
update ttt set id=10 where id=1;

如果會話1的隔離級別不是默認的,那么在執行update的過程中,可能會遇到以下錯誤:

ERROR 1665 (HY000): Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.

這是read committed和read uncommitted兩個隔離級別只允許row格式的二進制日志記錄格式。而當前的二進制日志格式記錄方式為statement時就會報錯。要解決這個問題,只要將格式設置為row或者mixed即可。

set @@session.binlog_format=row;

在會話2執行:

set transaction isolation level read uncommitted;
select * from ttt;
+------+
| id   |
+------+
|   10 |
|    2 |
+------+

發現查詢的結果是update后的數據,但是這個數據是會話1未提交的數據。這是臟讀的問題,即讀取了未提交的臟數據。

如果此時會話1進行了回滾操作,那么會話2上查詢的結果又變成了id=1。

在會話1上執行:

rollback;

在會話2上查詢:

mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

這是讀不一致問題。即同一個會話中對同一條記錄的讀取結果不一致。

read uncommitted一般不會在生產環境中使用,因為問題太多,會導致臟讀、丟失的更新、幻影讀、讀不一致的問題。但由於不申請讀鎖,從理論上來說,它的並發性是最佳的。所以在某些特殊情況下還是會考慮使用該級別。

要解決臟讀、讀不一致問題,只需在查詢記錄的時候加上共享鎖即可。這樣在其他事務更新數據的時候就無法查詢到更新前的記錄。這就是read commmitted隔離級別。

7.3 read committed

對於熟悉SQL Server的人來說,在說明這個隔離級別之前,必須先給個提醒:MariaDB/MySQL中的提交讀和SQL Server中的提交讀完全不一樣,MariaDB/MySQL中該級別基本類似於SQL Server中基於快照的提交讀

在SQL Server中,提交讀的查詢會申請共享鎖,並且在查詢結束的一刻立即釋放共享鎖,如果要查詢的記錄正好被獨占鎖鎖住,則會進入鎖等待,而沒有被獨占鎖鎖住的記錄則可以正常查詢。SQL Server中基於快照的提交讀實現的是語句級的事務一致性,每執行一次操作事務序列號加1,並且每次查詢的結果都是最新提交的行版本快照。

也就是說,MariaDB/MySQL中read committed級別總是會讀取最新提交的行版本。這在MySQL的innodb中算是一個術語:"一致性非鎖定讀",即只讀取快照數據,不加共享鎖。這在前文已經說明過。

MariaDB/MySQL中的read committed隔離級別下,除非是要檢查外鍵約束或者唯一性約束需要用到gap lock算法,其他時候都不會用到。也就是說在此隔離級別下,一般來說只會對行進行鎖定,不會鎖定范圍,所以會導致幻影讀問題。

這里要演示的就是在該級別下,會不斷的讀取最新提交的行版本數據。

當前示例表ttt的記錄如下:

mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

在會話1中執行:

begin;update ttt set id=100 where id=1;

在會話2中執行:

set @@session.tx_isolation='read-committed';
begin;
select * from ttt;

會話2中查詢得到的結果為id=1,因為查詢的是最新提交的快照數據,而最新提交的快照數據就是id=1。

+------+
| id   |
+------+
|    1 |
|    2 |
+------+

現在將會話1中的事務提交。

在會話1中執行:

commit;

在會話2中查詢記錄:

select * from ttt;
+------+
| id   |
+------+
|  100 |
|    2 |
+------+

結果為id=100,因為這個值是最新提交的。

再次在會話1中修改該值並提交事務。

在會話1中執行:

begin;update ttt set id=1000 where id=100;commit;

在會話2中執行:

select * from ttt;
+------+
| id   |
+------+
| 1000 |
|    2 |
+------+

發現結果變成了1000,因為1000是最新提交的數據。

read committed隔離級別的行版本讀取特性,在和repeatable read隔離級別比較后就很容易理解。

7.4 repeatable read

同樣是和上面一樣的廢話,對於熟悉SQL Server的人來說,在說明這個隔離級別之前,必須先給個提醒:MariaDB/MySQL中的重復讀和SQL Server中的重復讀完全不一樣,MariaDB/MySQL中該級別基本類似於SQL Server中快照隔離級別

在SQL Server中,重復讀的查詢會申請共享鎖,並且在查詢結束的一刻不釋放共享鎖,而是持有到事務結束。所以會造成比較嚴重的讀寫並發問題。SQL Server中快照隔離級別實現的是事務級的事務一致性,每次事務開啟的時候獲取最新的已提交行版本,只要事務不結束,讀取的記錄將一直是該行版本中的數據,不管其他事務是否已經提交過對應的數據了。但是SQL Server中的快照隔離會有更新沖突:當檢測到兩邊都想要更新同一記錄時,會檢測出更新沖突,這樣會提前結束事務(進行的是回滾操作)而不用再顯式地commit或者rollback。

也就是說,MariaDB/MySQL中repeatable read級別總是會在事務開啟的時候讀取最新提交的行版本,並將該行版本一直持有到事務結束但是MySQL中的repeatable read級別下不會像SQL Server一樣出現更新沖突的問題。

前文說過read committed隔離級別下,讀取數據時總是會去獲取最新已提交的行版本。這是這兩個隔離級別在"一致性非鎖定讀"上的區別。

另外,MariaDB/MySQL中的repeatable read的加鎖方式是next-key lock算法,它會進行范圍鎖定。這就避免了幻影讀的問題(官方手冊上說無法避免)。在標准SQL中定義的隔離級別中,需要達到serializable級別才能避免幻影讀問題,也就是說MariaDB/MySQL中的repeatable read隔離級別已經達到了其他數據庫產品(如SQL Server)的serializable級別,而且SQL Server中的serializable加范圍鎖時,在有索引的時候式鎖范圍比較不可控(你不知道范圍鎖鎖住哪些具體的范圍),而在MySQL中是可以判斷鎖定范圍的(見innodb鎖算法)。

這里要演示的就是在該級別下,讀取的行版本數據是不隨提交而改變的。

當前示例表ttt的記錄如下:

mysql> select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

在會話1執行:

begin;update ttt set id=100 where id=1

在會話2中執行:

set @@session.tx_isolation='repeatable-read';
begin;select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

查詢的結果和預期的一樣,來自開啟事務前最新提交的行版本數據。

回到會話1提交事務:

commit;

再回到會話2中查詢:

select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

再次去會話1更新該記錄:

begin;update ttt set id=1000 where id=100;commit;

再回到會話2執行查詢:

select * from ttt;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+

發現結果根本就不會改變,因為會話2開啟事務時獲取的行版本的id=1,所以之后讀取的一直都是id=1所在的行版本。

7.5 serializable

在SQL Server中,serializable隔離級別會將查詢申請的共享鎖持有到事務結束,且申請的鎖是范圍鎖,范圍鎖的情況根據表有無索引而不同:無索引時鎖定整個表,有索引時鎖定某些范圍,至於鎖定哪些具體的范圍我發現是不可控的(至少我無法推測和計算)。這樣就避免了幻影讀的問題。

這種問題在MariaDB/MySQL中的repeatable read級別就已經實現了,MariaDB/MySQL中的next-key鎖算法在加范圍鎖時也分有無索引:無索引時加鎖整個表(實際上不是表而是無窮大區間的行記錄),有索引時加鎖部分可控的范圍。

MariaDB/MySQL中的serializable其實類似於repeatable read,只不過所有的select語句會自動在后面加上lock in share mode。也就是說會對所有的讀進行加鎖,而不是讀取行版本的快照數據,也就不再支持"一致性非鎖定讀"。這樣就實現了串行化的事務隔離:每一個事務必須等待前一個事務(哪怕是只有查詢的事務)結束后才能進行哪怕只是查詢的操作。

這個隔離級別對並發性來說,顯然是有點太嚴格了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM