MariaDB—— 15. 事務

事務中的所有sql語句被當做一個操作單元,換句話說,事務中的sql語句要麼都執行成功,要麼全部執行失敗,事務內的sql語句被當做一個整體,被當做一個原子進行操作。

mysql中,innodb存儲引擎是支持事務的,而且innodb存儲引擎的事務完全符合ACID的特性,ACID是如下四大特性的首字母縮寫。
A:atomicity 原子性
C:consistency 一致性
I:isolation 隔離性
D:durability 持久性

原子性:整個事務中的所有操作要麼全部執行成功,要麼全部執行失敗後混滾到最初狀態。
一致性:數據庫總是從一個一致性狀態轉爲另一個一致性狀態。
隔離性:一個事務在提交之前所做出的的操作是否能爲其他事務可見,由於不同的場景需求不同,所以針對隔離性來說,有不同的隔離級別。
持久性:事務一旦提交,事務所做出的修改將會永久保存,此時即使數據庫崩潰,修改的數據也不會丟失。

1. redo log

mysql會將事務中的sql語句涉及到的所有數據操作先記錄到redo log中,然後再將操作從redo log中同步到對應數據文件中(此處假設事物操作的數據量並非巨大),換句話說,在事務執行提交成功以前,在修改對應的數據文件中的記錄之前,一定要保證 對應的所有修改操作已經記錄到了redo log中,假設事務中的sql語句涉及到60條記錄的修改,那麼在修改這60條記錄之前,要將這60條修改操作記錄到redo log中,當這60條操作都記錄到redolog中以後,再從redo log中一條一條同步到數據文件的對應記錄中。所以,即使數據文件中的數據被修改到一半時被打斷(比如停電),那麼也能依靠redo log中的日誌將剩餘的部分操作再次同步到對應的數據文件中。

使用redo log,能夠實現ACID中的A,也就是原子性,即事務中的所有sql被當做一個執行單元。
redo log其實由兩部分組成:redo log buffer(重做日誌緩衝) 和 redo log file(重做日誌文件)
redo log buffer存在於內存之中,是易失的,redo log file是持久的,存在於磁盤上。
重做日誌先被寫入到redo log buffer中,雖然內存的速度極快,但是無法滿足持久性的需求,因爲內存中的數據是易失的,所以爲了滿足持久性,需要將redo log buffer中的日誌寫入到redo log file中,相當於從內存中同步到磁盤上,所以磁盤的性能會影響事務的性能,由於redo log file是磁盤上一段連續的空間,所以寫速度還是比較快的,比離散的寫操作要快很多,當操作記錄被記錄到redo log file中以後,再從redo log file中將操作同步到數據文件中。
雖然,我們應該實時將redo log buffer中的數據寫入到redo log file中以保證數據的安全性,但是這樣會極大的降低性能,我們可以通過設置innodb_flush_log_at_trx_commit參數來修改從 redo log buffer寫入redo log file的策略,但是如果這樣做,則會喪失持久性,有可能會丟失部分數據,具體使用怎樣的刷寫策略,還需要根據實際情況自己權衡。

redo log是物理日誌,之所以說它是物理日誌,是因爲redo log 中記錄的是數據庫對頁的操作,而不是邏輯上的增刪改查,重做日誌具有冪等性。

2. undo log

可以把undo log理解成數據被修改前的備份。如果說事務進行了一半,有一條sql沒有執行成功,那麼數據庫可以根據undo log進行撤銷,將所有修改過的數據從邏輯上恢復到修改之前的樣子,注意,是邏輯上還原成原來的樣子,比如,之前insert了1000條數據 ,那麼就delete它們,如果delete了2000條,就insert它們,如果update了500條數據,就再次根據undo log去update它們,所以,undo log是邏輯日誌,與redo log記錄的頁操作物理日誌不同。

3. log group

log group爲重做日誌組,一個重做日誌組(log group)中有多個重做日誌文件(redo log file),當日志組中的第一個logfile被寫滿,則會開始將redo log寫入日誌組中的下一個重做日誌文件中,以此類推,當日志組中的所有redo log file都被寫滿,則將redo log再寫入第一個redo log file 中,覆蓋原來的redo log,以便新的redo log 被寫入。
如果重做日誌所在的設備崩潰了,那麼redo log將有可能丟失,這樣就無法保證redo log在任何時候都是可用的,所以,log group還支持日誌組鏡像,爲了保險起見,我們應該將log group放在有冗餘能力的設備上,比如raid1。
redo log存儲於重做日誌文件中,undo log則不同,undo存放在數據庫內部的特殊段中,這個段被稱爲undo段(undo segment),undo段位於共享表空間中。
不管是redo log或者undo log,都是innodb的產物,或者說是innodb存儲引擎層面的產物,而在mysql中,還有另外一種重要的日誌,二進制日誌,也就是平常所說的 binlog,它是建立mysql主從複製環境時所必須的日誌,但是binlog並不是innodb存儲引擎層面的產物,而是整個mysql數據庫層面的 產物,換句話說,binlog不止針對於innodb,mysql數據庫中的任何存儲引擎對於數據庫的更改都會產生二進制日誌(binlog)。
innodb的redo log記錄的是物理格式的日誌,記錄了對頁的操作,而binlog記錄的是邏輯日誌,記錄的是對應的SQL。
redolog與binlog寫入磁盤的時機也不同,innodb的redo log在事務進行時會不斷的寫入redo log file,binlog只在事務提交完成後進行一次磁盤寫入。
其實,不管是redo log 還是 undo log,都可以理解成恢復數據庫的手段。

4. 事務日誌參數

mysql> show global variables like '%innodb%log%';
+----------------------------------+------------+
| Variable_name                    | Value      |
+----------------------------------+------------+
| innodb_api_enable_binlog         | OFF        |
| innodb_flush_log_at_timeout      | 1          |
| innodb_flush_log_at_trx_commit   | 1          |
| innodb_locks_unsafe_for_binlog   | OFF        |
| innodb_log_buffer_size           | 16777216   |
| innodb_log_checksums             | ON         |
| innodb_log_compressed_pages      | ON         |
| innodb_log_file_size             | 50331648   |
| innodb_log_files_in_group        | 2          |
| innodb_log_group_home_dir        | ./         |
| innodb_log_write_ahead_size      | 8192       |
| innodb_max_undo_log_size         | 1073741824 |
| innodb_online_alter_log_max_size | 134217728  |
| innodb_undo_log_truncate         | OFF        |
| innodb_undo_logs                 | 128        |
+----------------------------------+------------+

innodb_log_file_size 表示每個redo log file的大小,單位爲字節,上圖中的設置表示每個重做日誌文件的大小爲5M
innodb_log_files_in_group 表示每個重做日誌組中有幾個redo log file
innodb_log_group_home_dir 表示重做日誌組文件所在路徑,此處的相對路徑表示數據所在目錄,默認情況下爲/var/lib/mysql,此目錄中的ib_logfile0與 ib_logfile1即爲日誌組中的兩個重做日誌,可以看到,這兩個日誌文件的大小爲5M,也對應了innodb_log_file_size的值
innodb_mirrored_log_groups 表示一共有幾組日誌組,1表示只有1組鏡像日誌組,就是當前日誌組本身,說白了,如果此值爲1,表示沒有冗餘的日誌組,如果想要有冗餘的鏡像日誌組,此值至少要設置爲2,此值容易被字面誤解,需注意,如果重做日誌所在的硬件設備並沒有冗餘能力,同時用戶對數 據安全性要求較高,那麼往往需要將此值設置爲大於等於2的值。
innodb_flush_log_at_trx_commit 表示當事務提交以後,是否立即將redo log從內存(log buffer)刷寫到redo log file中。
如果此值設置爲1(默認值),表示事務提交時必須將redo log從log buffer中刷寫到redologfile(磁盤)中,過程爲:事務提交–log buffer–os buffer–log file,此值爲1時完全滿足ACID的要求。
如果此值設置爲0,事務提交時並不會將redo log從log buffer刷寫到redo log file,但是會在每秒鐘自動刷寫一次,也就是說每一秒鐘都自動將內存中的redo log刷寫到redo log file(磁盤)中,可以理解爲,當事務提交時,redo log存在於log buffer中,每秒鐘,log從log buffer中經過os buffer,刷寫到log file中一次,當此值設置爲0時,如果mysql數據庫崩潰,最多會丟失1秒鐘的redo log。
如果此值設置爲2,表示在事務提交時,只會將redo log寫入到文件系統內存(os buffer)中,但是不會立即寫入到redo log file(磁盤)中,而是每秒鐘從文件系統緩存中將數據刷寫至redo log file(磁盤)中一次,可以理解爲,當事務提交時,redo log存在於log buffer和os buffer中,每秒鐘,log從os buffer中刷寫到log file中一次,此值爲2時,如果只是mysql數據庫宕機,但是操作系統沒有宕機,則數據不會丟失,如果此時操作系統宕機,重啓數據庫後,則會丟失未從 文件系統內存刷寫到redo log file中的那部分事務(約1秒鐘的數據),因爲只有mysql宕機而操作系統沒有宕機時,並不會丟失數據,所以可靠性 比此值設置爲0時要高一些。
理論上來說,此值設置爲1,安全性最高,性能最低,設置爲0,性能最高,安全性最低,設置爲2,性能較高,安全性較低,此值設置爲1,能夠滿足 ACID的特性,設置爲0或2,將會失去ACID的特性。但是需要注意,很多操作系統或者硬盤設備會欺騙mysqld進程,讓mysqld進程認爲刷寫操 作已經完成,但是實際上並沒有,在這種情況下,即使innodb_flush_log_at_trx_commit的值設置爲1,也不能保證事務的可用 性。

5. 事務控制語句

每執行一條sql語句,mysql都會把這條sql當做一個單語句事務進行提交,而且默認是自動提交的,我們可以使用如下語句查看mysql是否開啓了自動提交功能,查看全局與當前會話是否開啓了自動提交功能。

mysql> show global variables like 'autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

mysql> show session variables like 'autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

start transaction 或者 begin :表示顯示的開始一個事務,雖然begin和start transaction都表示顯式的開啓一個事務,但是在存儲過程中,mysql會將begin識別爲begin···end,所以,在存儲過程中,只能 使用start transaction來表示開始一個事務。
commit 或者 commit work :表示提交事務,也就是說從begin到commit之間的所有sql語句對數據庫所作出的修改將會被真正的執行,成爲永久性的操作。
rollback 或者 rollback work :表示回滾事務,回滾事務會撤銷所有未提交的修改並結束當前事務,注意,使用rollback回滾事務以後,當前事務會結束,後面的操作不算在當前事務以內。
savepoint 標識符 :表示創建一個事務的保存點,以便我們回滾到當前保存點,而不是回滾整個事務,就好比我們的遊戲存檔一樣,如果你在當前位置設置了保存點,那麼當你game over的時候,可以從這個保存點繼續,而不是從遊戲的開始處繼續,一個事務中可以創建多個保存點。
rollback to savepoint 標識符 :表示根據標識符回滾到指定。
release savepoint 標識符 :表示刪除一個保存點。

mysql> desc test2;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | NO   | PRI | NULL    |       |
| name  | varchar(30) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> select * from test2;
Empty set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test2(id,name) values  (1,'blueicex'),(2,'gege');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
Empty set (0.00 sec)

mysql> select * from test2;
Empty set (0.00 sec)

mysql> insert into test2(id,name) values  (1,'blueicex'),(2,'gege');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | gege     |
+----+----------+
2 rows in set (0.00 sec)

mysql> savepoint a;
Query OK, 0 rows affected (0.00 sec)

mysql>  insert into test2(id,name) values  (3,'name1'),(4,'name2');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | gege     |
|  3 | name1    |
|  4 | name2    |
+----+----------+
4 rows in set (0.00 sec)
mysql> rollback to savepoint a;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | gege     |
+----+----------+
2 rows in set (0.00 sec)
#savapoint不可逆
mysql> rollback to savepoint b;
ERROR 1305 (42000): SAVEPOINT b does not exist
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

6. 事務隔離級別

mysql中,innodb所提供的事務符合ACID的要求,而事務通過事務日誌中的redo log和undo log滿足了原子性、一致性、持久性,事務還會通過鎖機制滿足隔離性,在innodb存儲引擎中,有不同的隔離級別,它們有着不同的隔離性。
使用show processlist語句,可以看到,已經有兩個線程鏈接到當前數據庫。

mysql> show processlist;
+----+----------+--------------+----------+---------+------+----------+------------------+
| Id | User     | Host         | db       | Command | Time | State    | Info             |
+----+----------+--------------+----------+---------+------+----------+------------------+
|  5 | root     | localhost    | blueicex | Query   |    0 | starting | show processlist |
|  9 | blueicex | master:36696 | NULL     | Sleep   |   20 |          | NULL             |
+----+----------+--------------+----------+---------+------+----------+------------------+
2 rows in set (0.00 sec)

如果兩個用戶對同一個表進行增刪改查,就會出現事務的隔離問題。
READ-UNCOMMITTED : 此隔離級別翻譯爲 “讀未提交”。
READ-COMMITTED : 此隔離級別翻譯爲 “讀已提交” 或者 “讀提交”。
REPEATABLE-READ : 此隔離級別翻譯爲 “可重複讀” 或者 “可重讀”。
SERIALIZABLE : 此隔離級別翻譯爲"串行化"。

而mysql默認設置的隔離級別爲REPEATABLE-READ,即 “可重讀”。使用如下語句可以查看當前設置的隔離級別。

mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
6.1 隔離級別:可重讀

用戶1

MySQL [blueicex]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [blueicex]> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)

MySQL [blueicex]> update test2 set name='2' where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [blueicex]> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.01 sec)

用戶2

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)

mysql> update test1 set name='1' where id=2;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
#不能改變,內容可重複讀,兩個事務,只能有一個可以改變記錄內容,另一個只能讀原來的數據,也就是說,一個事務用戶,獨佔了寫表。具有寫鎖。
mysql> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)
 

用戶1

MySQL [blueicex]> commit;
Query OK, 0 rows affected (0.00 sec)

MySQL [blueicex]> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

用戶1和用戶2都同時提交了事務,才能看到用戶1改變的數據。在可重讀情況下,只允許一個事務或用戶更改數據,另一個用戶只能讀到未提交的數據。

用戶2

mysql> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)
mysql> commit ;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

用戶1

MySQL [blueicex]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [blueicex]> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

MySQL [blueicex]> insert into test2(id,name) values(3,'test');
Query OK, 1 row affected (0.00 sec)
 

用戶2

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test3;
ERROR 1146 (42S02): Table 'blueicex.test3' doesn't exist
mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

#由於用戶1沒有提交任務前,用戶1獨佔了表,所以用戶2不能夠更改表的內容,一直等待,直到timeout
mysql> update test2 set name='test3';

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> 
mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec

但是,假如用戶1和用戶2同時佔有一個表,用戶1提交了事務,用戶2並沒有提交,此時,就可能在用戶查詢或更新同一個表時,出現幻讀。幻讀不影響第用戶2的寫的結果。

6.2 隔離級別:串行化

用戶1獨佔數據表,用戶2無法在用戶1提交數據前,讀寫同一個數據表中的數據。此時數據庫沒有併發能力。

6.3 隔離級別:讀已提交

兩個用戶佔用1個表,第一個用戶,提交事務前後,用戶2在自己單獨的事務中,都能讀到用戶1修改的數據的變化。這種情況下,肯定會出現幻讀。
在讀已提交的隔離級別下,除了會出現幻讀的情況,還會出現不可重讀的情況。其實,“不可重讀"與"幻讀"的表象都非常相似,都是在同一個事務中,並沒有操作某些數據,可是這些數據卻莫名的被改變了,或者突然多出了某些數 據,又或者突然少了某些數據,這些狀況好像都能用"幻象"這個詞去理解,所以我一開始總是分不清到底什麼是幻讀,而且,mysql官方文檔中也把不可重讀 歸爲幻讀,只是大家爲了更加的細化它們的區別,把他們分成了"不可重讀"與"幻讀”,如果我們實在無法分清他們,我們可以這樣理解,"幻讀"的重點在於莫 名其妙的增加了或減少了某些數據,"不可重讀"的重點在於莫名的情況下,數據被修改更新了。

6.4 隔離級別:讀未提交

事務1上變化的數據,在事務未提交前,事務2都可見。此時會出現髒讀、幻讀和不可重複讀。
在這裏插入圖片描述

————Blueicex 2020/3/28 22:37 [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章