MySQL数据库:事务和ACID实现原理
一、什么是事務:
????????數(shù)據(jù)庫的事務是并發(fā)控制的基本單位,是指邏輯上的一組操作,要么全部執(zhí)行,要么全部不執(zhí)行。
1、事務的特性:
(1)原子性:事務是一個不可分割的工作單元,事務里的操作要么都成功,要么都失敗,如果事務執(zhí)行失敗,則需要進行回滾。
(2)隔離性:事務的所操作的數(shù)據(jù)在提交之前,對其他事務的可見程度。
(3)持久性:一旦事務提交,它對數(shù)據(jù)庫中數(shù)據(jù)的改變就是永久的。
(4)一致性:事務不能破壞數(shù)據(jù)的完整性和業(yè)務的一致性。例如在轉賬時,不管事務成功還是失敗,雙方錢的總額不變。
二、事務ACID特性的實現(xiàn)原理:
1、原子性:
????????原子性是通過MySQL的回滾日志undo log來實現(xiàn)的:當事務對數(shù)據(jù)庫進行修改時,InnoDB會生成對應的undo log;如果事務執(zhí)行失敗或調用了rollback,導致事務需要回滾,便可以利用undo log中的信息將數(shù)據(jù)回滾到修改之前的樣子。
undo log (回滾日志):是采用段(segment)的方式來記錄的,每個undo操作在記錄的時候占用一個undo log segment。在數(shù)據(jù)更改操作時,記錄了相對應的undo log的目的在于:
- 保證數(shù)據(jù)的原子性,記錄事務發(fā)生之前的一個版本,用于回滾;
- 通過mvcc?+?undo log實現(xiàn)Innodb事務可重復讀和讀已提交隔離級別。
2、隔離性:
隔離性是事務所操作的數(shù)據(jù)在提交之前,對其他事務的可見程度。
2.1、事務隔離級別:
????????為保證在并發(fā)環(huán)境下讀取數(shù)據(jù)的完整性和一致性,數(shù)據(jù)庫提供了四種事務隔離級別,隔離級別越高,越能保證數(shù)據(jù)的完整性和一致性,但對高并發(fā)性能影響也越大,執(zhí)行效率越低。(四種隔離級別從上往下依次升高)
讀未提交:允許事務在執(zhí)行過程中,讀取其他事務尚未提交的數(shù)據(jù);
讀已提交:允許事務在執(zhí)行過程中讀取其他事務已經(jīng)提交的數(shù)據(jù);
可重復讀(默認級別):在同一個事務內(nèi),任意時刻的查詢結果都是一致的;
讀序列化:所有事務逐個依次執(zhí)行,每次讀都需要獲取表級共享鎖,讀寫會相互阻塞。
2.2、如果不考慮事務的隔離性,在事務并發(fā)的環(huán)境下,可能存在問題有:
(1)更新丟失:兩個或多個事務操作相同的數(shù)據(jù),然后基于選定的值更新該行時,由于每個事務都不知道其他事務的存在,就會發(fā)生丟失更新問題:最后的更新覆蓋了其他事務所做的更新。
(2)臟讀:指事務A正在訪問數(shù)據(jù),并且對數(shù)據(jù)進行了修改(事務未提交),這時,事務B也使用這個數(shù)據(jù),后來事務A撤銷回滾,并把修改后的數(shù)據(jù)恢復原值,B讀到的數(shù)據(jù)就與數(shù)據(jù)庫中的數(shù)據(jù)不一致,即B讀到的數(shù)據(jù)是臟數(shù)據(jù)。
(3)不可重復讀:在一個事務內(nèi),多次讀取同一個數(shù)據(jù),但是由于另一個事務在此期間對這個數(shù)據(jù)做了修改并提交,導致前后讀取到的數(shù)據(jù)不一致;
(4)幻讀:在一個事務中,先后兩次進行讀取相同的數(shù)據(jù)(一般是范圍查詢),但由于另一個事務新增或者刪除了數(shù)據(jù),導致前后兩次結果不一致。
① 不可重復讀和幻讀的區(qū)別:
不可重復讀側重于讀取到其他事務修改的數(shù)據(jù),幻讀側重于讀取到其他事務新增或者刪除的數(shù)據(jù)。
② 可以采用鎖機制來解決不可重復讀和幻讀:
對于不可重復讀,只需對操作的數(shù)據(jù)添加行級鎖,防止操作的數(shù)據(jù)發(fā)生變化;而對于幻讀,需要添加表級鎖,將整張表鎖定,防止新增或者刪除數(shù)據(jù)。
不同的事務隔離級別,在并發(fā)環(huán)境會存在不同的并發(fā)問題:
| √:可能出現(xiàn)的情況? ×:不會出現(xiàn)該情況 | 臟讀 | 不可重復讀 | 幻讀 |
| 讀序列化 | × | × | × |
| 可重復讀 | × | × | √ |
| 讀已提交 | × | √ | √ |
| 讀未提交 | √ | √ | √ |
2.3、事務隔離性的實現(xiàn)原理:
????????為了實現(xiàn)事務隔離,數(shù)據(jù)庫延伸出了數(shù)據(jù)庫鎖,其中Innodb事務的隔離級別是由鎖機制和MVVC(多版本并發(fā)控制)實現(xiàn)的:
2.3.1、Mysql鎖機制:
????????MySQL鎖機制的基本工作原理就是:事務在修改數(shù)據(jù)庫之前,需要先獲得相應的鎖,獲得鎖的事務才可以修改數(shù)據(jù);在該事務操作期間,這部分的數(shù)據(jù)是鎖定,其他事務如果需要修改數(shù)據(jù),需要等待當前事務提交或回滾后釋放鎖。(鎖機制的更多說明可以參考另一篇博客:MySQL數(shù)據(jù)庫:鎖機制)
????????通過對InnoDB不同鎖類型的特性分析,可以利用鎖解決臟讀、不可重復讀、幻讀:
-
排它鎖解決臟讀:在讀已提交的隔離級別下,事務A只有在對數(shù)據(jù)修改時才加排它鎖,但直到事務?commit 時才釋放鎖。因此,同時進行的事務B希望讀取同一行數(shù)據(jù)時,會被事務A的排它鎖堵塞,所以解決了臟讀的問題
-
共享鎖解決不可重復讀:在可重復讀的隔離級別下,除了執(zhí)行讀已提交的排它鎖方式,還會在讀取一行數(shù)據(jù)時,為這行數(shù)據(jù)添加共享鎖直至事務 commit。例如,事務A讀取ID=1這一行數(shù)據(jù),然后為ID=1添加共享鎖,事務B同時希望update ID=1,此時獲取寫鎖失敗,因此在事務A執(zhí)行完之前,沒有其他任何事務可以對ID=1這一行做修改,因此解決了重復讀的問題
-
臨鍵鎖解決幻讀
????????雖然共享鎖和排它鎖解決了事務隔離的并發(fā)問題,但鎖會導致大量的堵塞,性能下降。某些時候會造成死鎖,為了解決死鎖,還要添加死鎖探測機制,性能進一步下降,因此需要更高效的方式實現(xiàn)事務的隔離級別,也就是 MVCC 多版本并發(fā)控制。
2.3.2、Multiversion concurrency control?(MVCC?多版本并發(fā)控制):
????????InnoDB 的?MVCC 的實現(xiàn)思路是:對每一行數(shù)據(jù)用 undo log 記錄多個版本,每個版本的數(shù)據(jù)可能都不相同,然后根據(jù) 事務ID 去尋找適合它的版本數(shù)據(jù),從而實現(xiàn)不同事務之間的隔離性。MVCC 具體實現(xiàn)實現(xiàn)方式是在每行記錄后面保存兩個隱藏的列:
- DB_TRX_ID:標識當前數(shù)據(jù)屬于哪個事務,每次提交事務,事務ID會自增,事務開始時會把該事務ID放到當前事務影響的行事務ID字段中,
- DB_ROLL_PTR:指向該行數(shù)據(jù) undo log 的指針,undo log 日志文件保存了該行記錄的所有版本數(shù)據(jù),并在日志中通過鏈表形式組織
????????通過 MVCC,數(shù)據(jù)庫可以使得事務的讀取不需要很多讀鎖,提升了數(shù)據(jù)庫的性能。當一個事務完成時,數(shù)據(jù)庫會刪除關于這條事務所有的 undo log,若未完成事務數(shù)據(jù)庫崩潰則根據(jù) undo log回滾,實現(xiàn)原子性。
????????MVCC只在 可重復度 和 讀已提交 兩個隔離級別下才會工作,其中,MVCC實質就是通過保存數(shù)據(jù)在某個時間點的快照來實現(xiàn)的。?
- 讀已提交:事務A執(zhí)行每一條語句時,生成一份活躍事務表,根據(jù)這份表去獲取數(shù)據(jù),因此不會獲取到臟數(shù)據(jù)(未提交事務引起的),但會有重復讀問題(因為可以讀取到commit 的事務數(shù)據(jù))
- 可重復讀:事務A只有在事務開始時才生成一份活躍事務表,因此不會讀取到事務A執(zhí)行中 commit 的其它事務引起的數(shù)據(jù)變更,也就不存在重復讀問題。
????????在并發(fā)訪問數(shù)據(jù)庫時,對正在事務中的數(shù)據(jù)做MVCC多版本的管理,以避免寫操作阻塞讀操作,并且可以通過比較版本解決快照讀方式的幻讀問題,但對于當前讀的幻讀,MVCC并不能解決,需要通過臨鍵鎖來解決。
- 快照讀:Innodb快照讀,數(shù)據(jù)的讀取將由 cache(原本數(shù)據(jù)) + undo(事務修改前的數(shù)據(jù)) 兩部分組成
- 當前讀:SQL讀取的數(shù)據(jù)是最新版本。通過鎖機制來保證讀取的數(shù)據(jù)無法通過其他事務進行修改
在可重復讀的隔離級別下,MVCC具體操作:
(1)SELECT操作:InnoDB遵循以下兩個規(guī)則:
- 只查找數(shù)據(jù)行的事務ID小于或等于當前事務ID的版本,這樣可以確保事務讀取的行,要么是在事務開始前已經(jīng)存在的,要么是事務自身插入或者修改過的記錄。
- 行的刪除版本要未被定義,讀取到事務開始之前狀態(tài)的版本,這可以確保事務讀取到的行,在事務開始之前未被刪除。只有同時滿足的兩者的記錄,才能返回作為查詢結果。
(2)INSERT:InnoDB為新插入的每一行保存當前事務編號作為行版本號。
(3)DELETE:InnoDB為刪除的每一行保存當前事務編號作為行刪除標識。
(4)UPDATE:InnoDB為插入一行新記錄,保存當前事務編號作為行版本號,同時保存當前事務編號到原來的行作為行刪除標識。
保存這兩個額外系統(tǒng)版本號,使大多數(shù)讀操作都可以不用加鎖。這樣設計使得讀數(shù)據(jù)操作很簡單,性能很好,并且也能保證只會讀取到符合標準的行,不足之處是每行記錄都需要額外的存儲空間,需要做更多的行檢查工作,以及一些額外的維護工作。
3、持久性:
????????持久性的實現(xiàn)關鍵在于redo log日志,在執(zhí)行SQL時會保存已執(zhí)行的SQL語句到一個指定的Log文件,當執(zhí)行recovery時重新執(zhí)行redo log記錄的SQL操作。
3.1、redo log日志:
????????當向數(shù)據(jù)庫寫入數(shù)據(jù)時,執(zhí)行過程會首先寫入Buffer Pool,Buffer Pool中修改的數(shù)據(jù)會定期刷新到磁盤中(這一過程叫做刷盤),這整一過程稱為redo log。redo log 分為:
- Buffer Pool內(nèi)存中的日志緩沖(redo log buffer),該部分日志是易失性的;
- 磁盤上的重做日志文件(redo log file),該部分日志是持久的。
????????Buffer Pool的使用可以大大提高了讀寫數(shù)據(jù)的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的數(shù)據(jù)在內(nèi)存還沒有刷新到磁盤,就會導致數(shù)據(jù)的丟失,事務的持久性無法保證。
????????為了確保事務的持久性,在當事務提交時,會調用fsync接口對redo log進行刷盤, (即redo log buffer寫日志到磁盤的redo log file中 ),刷新頻率由 innodb_flush_log_at_trx_commit變量來控制的:
- 0:?每秒刷新緩沖池中的數(shù)據(jù)寫入到磁盤中的,當系統(tǒng)崩潰,會丟失1秒鐘的數(shù)據(jù) ;
- 1: 事務每次提交的時候,就把緩沖池中的數(shù)據(jù)刷新到磁盤中;
- 2:提交事務的時候,把緩沖池中的數(shù)據(jù)寫入磁盤文件對應的 os cache 緩存里去,而不是直接進入磁盤文件。可能 1 秒后才會把 os cache 里的數(shù)據(jù)寫入到磁盤文件里去。
4、一致性:
一致性指的是事務不能破壞數(shù)據(jù)的完整性和業(yè)務的一致性 :
-
數(shù)據(jù)的完整性: 實體完整性、列完整性(如字段的類型、大小、長度要符合要求)、外鍵約束等
-
業(yè)務的一致性:例如在銀行轉賬時,不管事務成功還是失敗,雙方錢的總額不變。
那是如何保證數(shù)據(jù)一致性的?其實數(shù)據(jù)一致性是通過事務的原子性、持久性和隔離性來保證的:
- 原子性:語句要么全執(zhí)行,要么全不執(zhí)行,是事務最核心的特性,事務本身就是以原子性來定義的;主要基于undo log實現(xiàn)
- 持久性:保證事務提交后不會因為宕機等原因導致數(shù)據(jù)丟失;主要基于redo log實現(xiàn)
- 隔離性:保證事務執(zhí)行盡可能不受其他事務影響;InnoDB默認的隔離級別是RR,RR的實現(xiàn)主要基于鎖機制(包含next-key lock)、MVCC(包括數(shù)據(jù)的隱藏列、基于undo log的版本鏈、ReadView)
總結
以上是生活随笔為你收集整理的MySQL数据库:事务和ACID实现原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL数据库:索引的实现原理
- 下一篇: MySQL数据库:SQL优化与索引优化