事務是MySQL等關系型數據庫區別於NoSQL的重要方面,是保證數據一致性的重要手段。
一、什么是事務
事務是由數據庫中一系列的訪問和更新組成的邏輯執行單元。事務的邏輯單元中可以是一條SQL語句,也可以是一段SQL邏輯,這段邏輯要么全部執行成功,要么全部執行失敗。
舉個最常見的例子,你早上出去買早餐,支付寶掃碼付款給早餐老板,這就是一個簡單的轉賬過程,會包含兩步:(1)從你的支付寶賬戶扣款10元;(2)早餐老板的賬戶增加10元。這兩步其中任何一步出現問題,都會導致整個賬務出現問題。
假如你的支付寶賬戶扣款10元失敗,早餐老板的賬戶增加成功,那你就Happy了,相當於馬雲請你吃早餐了;
假如你的支付寶賬戶扣款10元成功,早餐老板的賬戶增加失敗,那你就悲劇了,早餐老板不會放過你,會讓你重新付款,相當於你請馬雲吃早餐了;
事務就是用來保證一系列操作的原子性,上述兩步操作,要么全部執行成功,要么全部執行失敗。
數據庫為了保證事務的原子性和持久性,引入了 redo log 和 undo log。
二、事務的4個特性
ACID是衡量事務的四個特性:
- 原子性(Atomicity,或稱不可分割性)
- 一致性(Consistency)
- 隔離性(Isolation)
- 持久性(Durability)
按照嚴格的標准,只有同時滿足ACID特性才是事務;但是在各大數據庫廠商的實現中,真正滿足ACID的事務少之又少。例如MySQL的NDB Cluster事務不滿足持久性和隔離性;InnoDB默認事務隔離級別是可重復讀,不滿足隔離性;Oracle默認的事務隔離級別為READ COMMITTED,不滿足隔離性……因此與其說ACID是事務必須滿足的條件,不如說它們是衡量事務的四個維度。
三、持久性 - redo log
1、持久性是指事務一旦提交,它對數據庫的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。
2、實現原理:采用 redo log
redo log 和 undo log 都屬於InnoDB的事務日志。下面先聊一下redo log存在的背景:InnoDB作為MySQL的存儲引擎,數據是存放在磁盤中的,但如果每次讀寫數據都需要磁盤IO,效率會很低。為此,InnoDB提供了緩存(Buffer Pool),Buffer Pool中包含了磁盤中部分數據頁的映射,作為訪問數據庫的緩沖:當從數據庫讀取數據時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool;當向數據庫寫入數據時,會首先寫入Buffer Pool,Buffer Pool中修改的數據會定期刷新到磁盤中(這一過程稱為刷臟)。
Buffer Pool的使用大大提高了讀寫數據的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的數據還沒有刷新到磁盤,就會導致數據的丟失,事務的持久性無法保證。
於是,redo log 被引入來解決這個問題:當數據修改時,除了修改Buffer Pool中的數據,還會在 redo log 記錄這次操作;當事務提交時,會調用fsync接口對redo log進行刷盤。如果MySQL宕機,重啟時可以讀取redo log中的數據,對數據庫進行恢復。redo log采用的是WAL(Write-ahead logging,預寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數據不會因MySQL宕機而丟失,從而滿足了持久性要求。
既然 redo log 也需要在事務提交時將日志寫入磁盤,為什么它比直接將Buffer Pool中修改的數據寫入磁盤(即刷臟)要快呢?主要有以下兩方面的原因:
(1)刷臟是隨機IO,因為每次修改的數據位置隨機,但寫redo log是追加操作,屬於順序IO。
(2)刷臟是以數據頁(Page)為單位的,MySQL默認頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。
3、redo log 介紹
redo log 是重做日志,通常是物理日志,記錄的是物理數據頁的修改,它用來恢復提交后的物理數據頁
如上圖所示,redo log分為兩部分:
(1)內存中的redo log Buffer是日志緩沖區,這部分數據是容易丟失的
(2)磁盤上的redo log file是日志文件,這部分數據已經持久化到磁盤,不容易丟失
SQL操作數據庫之前,會先記錄重做日志,為了保證效率會先寫到日志緩沖區中(redo log Buffer),再通過緩沖區寫到磁盤文件中進行持久化,既然有緩沖區說明數據不是實時寫到redo log file中的,那么假如redo log寫到緩沖區后,此時服務器斷電了,那redo log豈不是會丟失?
在MySQL中可以自已控制log buffer刷新到log file中的頻率,通過innodb_flush_log_at_trx_commit參數可以設置事務提交時log buffer如何保存到log file中,innodb_flush_log_at_trx_commit參數有3個值(0、1、2),表示三種不同的方式:
(1)為1:表示事務每次提交都會將log buffer寫入到os buffer,並調用操作系統的fsync()方法將日志寫入log file,這種方式的好處是就算MySQL崩潰也不會丟數據,redo log file保存了所有已提交事務的日志,MySQL重新啟動后會通過redo log file進行恢復。但這種方式每次提交事務都會寫入磁盤,IO性能較差。
(2)為0:表示事務提交時不會將log buffer寫入到os buffer中,而是每秒寫入os buffer然后調用fsync()方法將日志寫入log file,這種方式在MySQL系統崩潰時會丟失大約1秒鍾的數據。
(2)為2:表示事務每次提交僅將log buffer寫入到os buffer中,然后每秒調用fsync()方法將日志寫入log file,這種方式在MySQL崩潰時也會丟失大約1秒鍾的數據。
四、原子性 - undo log
1、原子性是指一個事務是一個不可分割的工作單位,其中的操作要么都做,要么都不做;如果事務中一個sql語句執行失敗,則已執行的語句也必須回滾,數據庫退回到事務前的狀態。
2、實現原理:采用 undo log
在說明原子性原理之前,首先介紹一下MySQL的事務日志。MySQL的日志有很多種,如二進制日志、錯誤日志、查詢日志、慢查詢日志等,此外InnoDB存儲引擎還提供了兩種事務日志:redo log(重做日志)和undo log(回滾日志)。其中redo log用於保證事務持久性;undo log則是事務原子性和隔離性實現的基礎。
下面說回undo log。實現原子性的關鍵,是當事務回滾時能夠撤銷所有已經成功執行的sql語句。
InnoDB實現回滾,靠的是undo log:當事務對數據庫進行修改時,InnoDB會生成對應的undo log;如果事務執行失敗或調用了rollback,導致事務需要回滾,便可以利用undo log中的信息將數據回滾到修改之前的樣子。
3、undo log 介紹
undo log 是回滾日志,用來回滾行記錄到某個版本,undo log一般是邏輯日志,根據行的數據變化進行記錄。
undo log 跟 redo log 一樣也是在SQL操作數據之前記錄的,也就是SQL操作先記錄日志,再進行操作數據。
如上圖所示,SQL操作之前會先記錄 redo log、undo log 到日志緩沖區,日志緩沖區的數據會記錄到os buffer中,再通過調用fsync()方法將日志記錄到log file中。
undo log 屬於邏輯日志,它記錄的是sql執行相關的信息。可以簡單的理解為:當insert一條記錄時,undo log會記錄一條對應的delete語句;當update一條語句時,undo log記錄的是一條與之操作相反的語句。當事務需要回滾時,可以從undo log中找到相應的內容進行回滾操作,回滾后數據恢復到操作之前的狀態。
當發生回滾時,InnoDB會根據undo log的內容做與之前相反的工作:對於每個insert,回滾時會執行delete;對於每個delete,回滾時會執行insert;對於每個update,回滾時會執行一個相反的update,把數據改回去。
以update操作為例:當事務執行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前后的值等信息,回滾時便可以使用這些信息將數據還原到update之前的狀態。
總結:MySQL中是如何實現事務提交和回滾的:
1、為了保證數據的持久性,數據庫在執行SQL操作數據之前會先記錄redo log和undo log;
2、redo log是重做日志,通常是物理日志,記錄的是物理數據頁的修改,它用來恢復提交后的物理數據頁;
3、undo log是回滾日志,用來回滾行記錄到某個版本,undo log一般是邏輯日志,根據行的數據變化進行記錄;
4、redo/undo log都是寫先寫到日志緩沖區,再通過緩沖區寫到磁盤日志文件中進行持久化保存;
5、undo日志還有一個用途就是用來控制數據的多版本(MVCC)
簡單理解就是:
redo log是用來恢復數據的,用於保障已提交事務的持久性;
undo log是用來回滾事務的,用於保障未提交事務的原子性。
五、隔離性
1、定義
與原子性、持久性側重於研究事務本身不同,隔離性研究的是不同事務之間的相互影響。隔離性是指,事務內部的操作與其他事務是隔離的,並發執行的各個事務之間不能互相干擾。嚴格的隔離性,對應了事務隔離級別中的Serializable (可串行化),但實際應用中出於性能方面的考慮很少會使用可串行化。
隔離性追求的是並發情形下事務之間互不干擾。簡單起見,我們主要考慮最簡單的讀操作和寫操作(加鎖讀等特殊讀操作會特殊說明),那么隔離性的探討,主要可以分為兩個方面:
- (一個事務)寫操作對(另一個事務)寫操作的影響:鎖機制保證隔離性
- (一個事務)寫操作對(另一個事務)讀操作的影響:MVCC保證隔離性
2、鎖機制
首先來看兩個事務的寫操作之間的相互影響。隔離性要求同一時刻只能有一個事務對數據進行寫操作,InnoDB通過鎖機制來保證這一點。
鎖機制的基本原理可以概括為:事務在修改數據之前,需要先獲得相應的鎖;獲得鎖之后,事務便可以修改數據;該事務操作期間,這部分數據是鎖定的,其他事務如果需要修改數據,需要等待當前事務提交或回滾后釋放鎖。
3、行鎖與表鎖
按照粒度,鎖可以分為表鎖、行鎖以及其他位於二者之間的鎖。表鎖在操作數據時會鎖定整張表,並發性能較差;行鎖則只鎖定需要操作的數據,並發性能好。但是由於加鎖本身需要消耗資源(獲得鎖、檢查鎖、釋放鎖等都需要消耗資源),因此在鎖定數據較多情況下使用表鎖可以節省大量資源。MySQL中不同的存儲引擎支持的鎖是不一樣的,例如MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出於性能考慮,絕大多數情況下使用的都是行鎖。
六、一致性
1、一致性是指事務執行結束后,數據庫的完整性約束沒有被破壞,事務執行的前后都是合法的數據狀態。
數據庫的完整性約束包括但不限於:實體完整性(如行的主鍵存在且唯一)、列完整性(如字段的類型、大小、長度要符合要求)、外鍵約束、用戶自定義完整性(如轉賬前后,兩個賬戶余額的和應該不變)。
2、實現原理
一致性是事務追求的最終目標:前面提到的原子性、持久性和隔離性,都是為了保證數據庫狀態的一致性。此外,除了數據庫層面的保障,一致性的實現也需要應用層面進行保障。實現一致性的措施包括:
- 保證原子性、持久性和隔離性,如果這些特性無法保證,事務的一致性也無法保證
- 數據庫本身提供保障,例如不允許向整形列插入字符串值、字符串長度不能超過列的限制等
- 應用層面進行保障,例如如果轉賬操作只扣除轉賬者的余額,而沒有增加接收者的余額,無論數據庫實現的多么完美,也無法保證狀態的一致
總結:ACID特性及其實現原理
1、原子性:語句要么全執行,要么全不執行,是事務最核心的特性,事務本身就是以原子性來定義的;實現主要基於undo log
2、持久性:保證事務提交后不會因為宕機等原因導致數據丟失;實現主要基於redo log
3、隔離性:保證事務執行盡可能不受其他事務影響;InnoDB默認的隔離級別是RR,RR的實現主要基於鎖機制(包含next-key lock)、MVCC(包括數據的隱藏列、基於undo log的版本鏈、ReadView)
4、一致性:事務追求的最終目標,一致性的實現既需要數據庫層面的保障,也需要應用層面的保障
參考文章:https://www.cnblogs.com/superming/p/13368771.html