一、事務概述
我們可以將事務理解為一組sql語句的集合。事務可以只包含一條sql,也可以包含多條sql,事務中所有sql語句被當作一個操作單元,要么全部執行成功,要么全部失敗。
mysql中 innodb存儲引擎支持事務,MyISAM不支持事務。并且Innodb存儲引擎的事務完全符合ACID特性。
ACID的理論基礎:
1、原子性:atomicity (整個事務中的所有操作要么全部成功,要么全部失敗回滾到最初狀態)
2、一致性:consistency (數據庫從一個一致性性狀態轉為另一個一直性狀態)
3、隔離性:isolation (一個事務在提交之前所作出的操作是否能被其他事務可見,不同場景需求不同,因此有多個隔離級別)
4、持久性:durability (事務一旦提交,事務所做出的修改將會永久保存,即使此時數據庫崩潰,修改數據也不會消失)
在講事務例子時多數都以銀行轉賬作為場景
例子:A賬戶余額 2000,B賬戶余額 1000 ,現在A用戶要向B用戶轉賬200元,那么轉賬之后A賬戶余額應該有1800,B賬戶余額應該有1200。這種情況看似沒有問題,但是如果不用事務可能會出現 A賬戶少了200,但是B賬戶并沒有加上200。
使用事務后整個過程如下:
事務開始
update A賬戶余額-200
update B賬戶余額+200
提交事務 (事務結束)
事務底層是怎樣實現的呢? 通過 “事務日志” 來實現的這種功能。事務日志分為 redo log 和 undo log 。
(1)、redo log
mysql會將事務中的sql語句涉及到的所有數據操作先記錄到redo log中,然后再將操作從redo log中同步到對應數據文件中。也就是說,在事務執行提交成功之前,在修改對應的數據文件中的記錄之前,一定要保證對應的所有修改操作已經記錄到 redo log中。假設事務中的sql語句涉及到10條記錄的修改,那么在修改這10條記錄之前,要將這10條修改操作記錄記錄到 redo log中,當這10條操作都記錄到 redo log中以后再從redo log中一條一條同步到數據文件的對應記錄中。所以即使數據文件中的數據被修改到一半時被打斷 (比如斷電),那么在恢復后也能依靠redo log日志將剩余部分操作再次同步到對應的數據文件中。
使用redo log,能夠實現ACID中的原子性。redo log 其實由兩部分組成:redo log buffer(重做日志緩沖)和 redo log file (重做日志文件) redo log buffer存在于內存之中,容易丟失,redo log file 是持久的,存在于硬盤上。
--------------> log buffer --------------> os buffer----------------->redo log file ----------------------->數據文件
重做日志先被寫入到 redo log buffer 中,雖然內存的速度極快,但是無法滿足持久性的需求,因為內存數據容易丟失,所以為了滿足持久性,需要將redo log buffer中的日志寫入到redo log file 中,相當于從內存同步到磁盤,所以磁盤的性能會影響事務的性能,由于 redo log file 是磁盤上一系列連續的空間,所以相對于離散的空間寫速度還是比較快,當操作被記錄到 redo log file中以后再從 redo logfile中將操作同步到數據文件中。
雖然,我們應該實時將 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了200條數據,那么就需要 delete這200條。所以說 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。
mysql中,innodb存儲引擎支持事務,myisam存儲引擎是不支持事務的,不管是redo log還是 undo log,都是innodb的產物。而在mysql中還有另外一種重要的日志,二進制日志,也就是平常所說的 binlog,他是建立mysql主從復制環境時所必須的日志,但是binlog并不是innodb存儲引擎層面的產物,而是整個mysql數據庫層面的產物,mysql任何存儲引擎對于數據庫的更改都會產生二進制日志 (binlog) 。
innodb的 redo log 記錄的是物理格式的日志,記錄了對頁的操作,而binlog記錄的是邏輯日志,記錄對應的sql
其實不管是redolog 還是 undo log 都可以理解成恢復數據庫的手段。
二、事務日志參數
當使用innodb存儲引擎時,我們可以通過如下語句查看日志配置參數:show global variables like '%innodb%log%';
innodb_log_file_size: 表示redo log file 的大小,單位為字節,上圖中的設置表示每個重做日志文件的大小為 5M
innodb_log_files_in_group :表示每個重做日志組中有幾個 redo log file
innodb_log_group_home_dir:表示重做日志組文件所在路徑。默認情況下為 /var/lib/mysql,windows系統下會在data目錄下,在這個目錄下有ib_logfile0與ib_logfile1即為日志組中兩個重要日志,這兩個日志文件大小為5M,對應了innodb_log_file_size的值。
innodb_mirrored_log_groups:表示一共有幾個日志組。如果設置為1表示沒有冗余的鏡像日志組。注意,如果重做日志所在的硬件設備并沒有冗余能力,同時用戶對數據安全性比較高,那么往往需要將此值設置為大于等于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,但是會在每秒鐘自動刷寫一次,也就是說如果mysql數據庫崩潰,最多會丟失1秒的redo log。
如果此值設置為2,當事務提交時,redolog存在于 log buffer和os buffer中,每秒從 os buffer中刷寫到log file中一次,這種情況下如果mysql宕機,操作系統沒有宕機時,并不會丟失數據,所以可靠性要比設置為0時要高一些。
理論上來說:此值設置為1,安全性最高,性能最低。設置為0,性能最高,安全性最低。設置為2,性能較高,安全性較低。此值為1時,能夠滿足ACID特性,其他兩個值則不滿足ACID。
我們可以根據自己的需要,設置重做日志的相關參數。
1、事務控制語句
在mysql中,默認情況下我們每執行一條sql語句,mysql都會把這條sql當做一個單語句事務進行提交,并且是默認自動提交的。我們可以使用如下語句查看:
show global variables like '%autocommit%';
show session variables like '%autocommit%';
如上圖所示,默認情況下,autocommit是開啟的,表示事務都是自動提交的。
如果需要手動控制提交,需要顯示的開啟一個事務,或者禁用自動提交功能 (set autocommit=0),進行手動提交。
(1)、start transaction 或者 begin :表示顯示的開啟一個事務,在存儲過程中不能用begin開啟一個事務。
(2)、commit 或者 commit work:表示提交事務,也就是從begin到commit之間所有sql語句對數據庫所做的修改會被真正執行
(3)、rollback 或者 rollback work:表示回滾事務,回滾事務會撤銷所有未提交的修改并結束當前事務。注意,使用rollback回滾事務以后,當前事務會結束,后面的操作不算在當前事務以內。
(4)、savepoint 標識符:表示創建一個事務保存點,以便我們回滾到當前保存點,而不是回滾整個事務。
(5)、rollback to savepoint 標識符:表示根據標識符回滾到指定的保存點,使用rollback to savepoint只會撤銷對應保存點之后的操作,并且不會結束當前事務,回滾到指定的保存點以后的操作仍然屬于當前事務,于rollback不同。
(6)、release savepoint 標識符:表示刪除一個保存點。
下面進行實際操作,演示一下效果:
先從最簡單事務開始:
下面演示事務回滾的例子:
事務開始之前,user表中有5條數據,事務開始以后我們刪除id為 4和5的兩條記錄,但是我們并沒有commit,所以這些操作沒有真正持久化到數據庫中,此時,我們執行了rollback,所有未提交的操作都被撤銷了,同時當前事務結束。
上面演示了事務的正常提交和回滾,下面演示一下保存點。
通過事務控制語句,即可顯示的手動的對事務進行控制,之前說過,我們可以禁用autocommit功能,從而進行手動的提交操作,示例如下:set @@session.autocommit=0;
因為關閉了自動提交功能,所以,每個sql語句并不會自動被當做一個單語句事務,所以每個sql語句并不會被自動提交。如果需要將之前的修改持久化,需要手動執行commit操作。
三、事務的隔離級別
1、事務隔離級別綜述
mysql中,innodb所提供的事務復合ACID的要求,而通過事務日志中的 redo log和undo log 滿足了原子性、一致性、持久性、事務還會通過鎖機制滿足隔離性,在innodb存儲引擎中,有不同的隔離級別,他們有著不同的隔離性。
下面使用兩個會話窗口演示隔離級別
此處,我們列出innodb中事務的所有隔離級別,然后逐個了解他們,事務隔離級別一共有如下四種:
1、READ-UNCOMMITTED:此隔離級別翻譯為:“讀未提交”
2、READ-COMMITTED:此隔離級別翻譯為:“讀已提交”
3、REPEATABLE-READ:此隔離級別翻譯為:“可重復讀”
4、SERIALIZABLE:此隔離級別翻譯為:“串行化”
Mysql的默認隔離級別為REPEATABLE-READ,即 “可重復讀”
查看mysql的隔離級別:show variables like 'tx_isolation';
如果需要修改配置文件 my.cnf配置文件,則可通過如下參數配置mysql的事務隔離級別
transaction_isolation=REPEATABLE-READ
下面詳細介紹各個隔離界別
1、可重讀
我們先來總結一下可重讀隔離級別的特性。仍然以上面例子為例,在事務1中修改了一條數據以后,事務2看到的數據仍然是事務1修改之前的數據,即使事務1提交了,在事務2沒有提交之前,事務2看到的數據還是相同的,所以這種隔離級別被稱為“可重讀”
但是你可能會有個問題,之前說過,事務的隔離性是由鎖來實現的,那么,當事務1執行更新語句時,事務1應該對數據加了寫鎖,但是在事務2中,仍然可以進行查詢操作,即進行讀操作,可是寫鎖是排他鎖,在事務1已經添加寫鎖的情況下,為什么事務2還可以讀取呢?這是因為innodb采用了一致性非鎖定讀的機制提高了數據庫并發性。一致性非鎖定讀表示如果當前行如果被施加了排他鎖,那么當需要讀取行數據時,則不會等待行上的鎖的釋放,而是會去讀取一個快照數據。
一行記錄可能有不止一個快照數據,并不是所有隔離級別都使用了一致性非鎖定讀,在“可重讀”和“讀提交”的隔離級別下,innodb存儲引擎使用了一致性非鎖定讀,但是在這兩個隔離級別中,對于快照的定義也不相同。在“可重讀”隔離級別下,快照數據是當前事務開始的樣子,但是在“讀提交”的隔離級別下,由于快照定義不同,所以顯示的現象也不同。
在可重讀隔離級別下,可能會出現“幻讀”的問題,那么什么是幻讀,我們一起看一下
從上圖可以看出,事務1commit之后,數據其實就已經發生了變化,但是事務2在沒有update之前無法看到變化的數據,但是當事務2更新數據以后,發現莫名多出了一條數據。在同一個事務中,執行兩次同樣的sql,第二次sql返回之前不存在的行,或者之前出現的數據不見了,這種現象被稱之為“幻讀”。
注意:事務2中的update語句并沒有指定任何條件,相當于更新所有行的對應字段,如果指定了條件,并沒有更新到“隱藏”行,那么可能無法看到幻讀現象
上述事務2還沒有進行commit操作,如果不執行commit操作,那么所有數據都會回滾。也就是事務2還沒有生效。
2、串行化
經過上述實例我們可以發現,事務處于“REPEATABLE-READ”可重讀級別時,會出現幻讀的情況,在之前我們提到,不同的隔離級別所引入的隔離性不同,那么有沒有一種隔離級別能夠解決幻讀的問題呢?答案是:串行化可以解決這個問題。我們來試試串行化時,事務是怎么樣工作的。
如上圖,將兩個會話窗口中的事務隔離級別設置為串行化后,分別開啟兩個事務,在事務1插入一條數據沒有提交的情況下,此時事務2執行查詢數據時好像是被卡主了,沒有任何反應
過一段時間后,會報一個錯:獲取鎖失敗
換另外一種實驗方式:在事務2被阻塞的時候,提交事務1,會發現事務2查詢立即返回結果。
從上述實驗來看,當事務處于串行化隔離級別時,是不可能出現幻讀的情況的,因為如果另一個事務對表添加了寫鎖,那么在當前事務是無法讀取到數據的,必須等到另一個事務提交或者回滾。使用串行化隔離級別不會出現幻讀,但是數據庫失去了并發的能力,所以我們很少將隔離級別設置為串行化,因為這種隔離性過于嚴格。
3、讀已提交
現在我們已經了解了兩種隔離級別,可重讀與串行化。串行化隔離級別的隔離性是最強的,沒有并發能力,可重讀隔離級別的隔離性稍微次之,但是比較串行化而言,并發能力較好,不過存在“幻讀”的問題。
同樣在兩個會話中開啟兩個事務,在事務1中修改數據,此時事務1還未提交,在事務2中并不能看到事務1中的修改,而當事務1提交以后,事務2中即可看到事務1中的修改,話句話說,就是事務2能夠讀到事務1提交后的更改,這種隔離級別被稱為“讀提交”。
在“讀提交”的隔離級別下,也會出現“幻讀”的問題,示例如下:
在上述示例中,事務1向表中插入了一行數據,在事務1提交以后,事務2中即可看到,但是事務2還沒有提交,在事務2中執行兩次相同的查詢語句,莫名其妙的多出了一行,出現了“幻讀”的情況。
在讀已提交隔離級別下,除了會出現幻讀的情況,還會出現不可重讀的情況。“不可重讀”表示“不一定可重讀”,不要理解為“一定不可重讀”。
上述例子中,事務1中修改了id=1的數據,提交以后,事務2中不能再讀到之前的數據了,所以出現了“不可重讀”的現象。
其實,總結一下這個隔離級別,是因為事務1的提交對事務2立即可見,所造成的“不可重讀”和“幻讀”的情況。
那么我們再來總結一下“讀提交”這個隔離級別,在“讀提交”隔離級別下,會出現“不可重讀”,“幻讀”的問題,比“可重讀”隔離級別問題更多,但是并發能力更強。現在就剩下一個隔離級別沒有了解到,那就是“讀未提交”。
4、讀未提交
還是以一個例子演示:
將隔離級別調整為“讀未提交”,可以看出事務1并沒有提交,但是事務2看到了事務1中鎖做出的修改。
當事務能夠看到別的事務中“未提交”的數據,我們稱這種現象為“臟讀”。
上例中,事務1并未提交,但是其所作出的修改已經能在事務2中查看到,由于事務1中的修改可能被回滾,或者繼續被修改,所以事務2中看到的數據飄忽不定,并不是最終的數據,所以是“臟的”。在事務隔離級別為“讀未提交”時,其并發性能最強,但是其隔離性與安全性是最差的。
因此,在事務處于“讀未提交”這種隔離級別時,會出現“臟讀”,同時也會出現“不可重讀”,“幻讀”的情況。
四、臟讀、幻讀、不可重讀的區別
我們來總結一下它們之間的區別:
臟讀:當前事務可以查看到其他事務未提交的數據(側重點在于別的事務未提交)
幻讀:幻讀的表象于不可重讀的表象都讓人“懵逼”,很容易搞混,但是如果非要細分,幻讀的側重點在于新增和刪除。表示在同一個事務中,使用相同的查詢語句,第二次查詢時,莫名的多出了一些之前不存在的數據,或者莫名的不見了一些數據。
不可重讀:不可重讀的側重點在于更新數據。表示在同一事務中,查詢相同的數據范圍時,同一個數據資源莫名的改變了。
五、不同隔離級別所擁有的問題
下面做一個最終的總結: