詳解MySQL事務(wù)的ACID如何實(shí)現(xiàn)
事務(wù)是什么?
事務(wù)(Transaction)是并發(fā)控制的基本單位。所謂的事務(wù)呢,它是一個(gè)操作序列,這些操作要么都執(zhí)行,要么都不執(zhí)行,它是一個(gè)不可分割的工作單位。
在介紹事務(wù)的特性之前,我們先看下MySQL的邏輯架構(gòu),
如上圖所示,MySQL服務(wù)器邏輯架構(gòu)從上往下可以分為三層:
- 第一層:處理客戶端連接、授權(quán)認(rèn)證等。
- 第二層:服務(wù)器層,負(fù)責(zé)查詢語句的解析、優(yōu)化、緩存以及內(nèi)置函數(shù)的實(shí)現(xiàn)、存儲過程等。
- 第三層:存儲引擎,負(fù)責(zé)MySQL中數(shù)據(jù)的存儲和提取。MySQL 中服務(wù)器層不管理事務(wù),事務(wù)是由存儲引擎實(shí)現(xiàn)的。**MySQL支持事務(wù)的存儲引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最為廣泛;其他存儲引擎不支持事務(wù),如MyISAM、Memory等。
后續(xù)討論主要以InnoDB為主。
事務(wù)有什么特征?
事務(wù)的特性,可以總結(jié)為如下4個(gè)方面:
原子性(Atomicity):原子性是指整個(gè)數(shù)據(jù)庫的事務(wù)是一個(gè)不可分割的工作單位,在每一個(gè)都應(yīng)該是原子操作。當(dāng)我們執(zhí)行一個(gè)事務(wù)的時(shí)候,如果在一系列的操作中,有一個(gè)操作失敗了,那么需要將這一個(gè)事務(wù)中的所有操作恢復(fù)到執(zhí)行事務(wù)之前的狀態(tài),這就是事務(wù)的原子性。
一致性(Consistency): 一致性呢是指事務(wù)將數(shù)據(jù)庫從一種狀態(tài)轉(zhuǎn)變成為下一種一致性的狀態(tài),也就是說是在事務(wù)的執(zhí)行前后,這兩種狀態(tài)應(yīng)該是一樣的,也就是在數(shù)據(jù)庫的完整性約束不會被破壞。另外的話,還需要注意的是一致性不關(guān)注中間的過程是發(fā)生了什么。
隔離性(lsolation): Mysql數(shù)據(jù)庫可以同時(shí)的話啟動很多的事務(wù),但是呢,事務(wù)跟事務(wù)之間他們是相互分離的,也就是互不影響的,這就是事務(wù)的隔離性。下面有介紹事務(wù)的四大隔離級別。
持久性(Durability): 事務(wù)的持久性是指事務(wù)一旦提交,就是永久的了。說白了就是發(fā)生了問題,數(shù)據(jù)庫也是可以恢復(fù)的。因此持久性保證事務(wù)的高可靠性。
談到事務(wù)的四大特性,不得不說一下MySQL事務(wù)的隔離機(jī)制,在不同的數(shù)據(jù)庫連接中,一個(gè)連接的事務(wù)并不會影響其他連接,這是基于事務(wù)隔離機(jī)制實(shí)現(xiàn)的。在MySQL
中,事務(wù)隔離機(jī)制分為了四個(gè)級別:
Read uncommitted / RU:讀未提交,就是一個(gè)事務(wù)可以讀取另一個(gè)未提交事務(wù)的數(shù)據(jù)。毫無疑問,這樣會造成大量的臟讀,所以數(shù)據(jù)庫一般不會采用這種隔離級別。
Read committed / RC:讀已提交,就是一個(gè)事務(wù)讀到的數(shù)據(jù)必須是其他事務(wù)已經(jīng)提交的數(shù)據(jù),這樣就避免了臟讀的情況。但是如果有兩個(gè)并行的事務(wù)A和B,處理同一批的數(shù)據(jù),如果事務(wù)A在這個(gè)過程中,修改了數(shù)據(jù)并提交;那么在事務(wù)B中可能前后看到兩個(gè)不一樣的數(shù)據(jù),這就造成不可重復(fù)讀的情況。
Repeatable read / RR:可重復(fù)讀,就是在開始讀取數(shù)據(jù)(事務(wù)開啟)時(shí),不再允許修改操作。這樣就解決了不可重復(fù)讀的問題,但是需要注意的是,不可重復(fù)讀對應(yīng)的是修改,即UPDATE操作。但是可能還會有幻讀問題。因?yàn)榛米x問題對應(yīng)的是插入INSERT操作,而不是UPDATE操作。
Serializable:序列化/串行化。它通過強(qiáng)制事務(wù)排序,使之不可能相互沖突,從而解決幻讀問題。簡言之,它是在每個(gè)讀的數(shù)據(jù)行上加上共享鎖。這種情況下所有事務(wù)串行執(zhí)行,可以避免上面的出現(xiàn)的各種問題,但是在大并發(fā)場景下會導(dǎo)致大量的超時(shí)現(xiàn)象和鎖競爭,所以一般也很少采用。
上述四個(gè)級別,越靠后并發(fā)控制度越高,也就是在多線程并發(fā)操作的情況下,出現(xiàn)問題的幾率越小,但對應(yīng)的也性能越差,MySQL
的事務(wù)隔離級別,默認(rèn)為第三級別:Repeatable read可重復(fù)讀。
按照嚴(yán)格的標(biāo)準(zhǔn),只有同時(shí)滿足ACID特性才是事務(wù);但是目前各大數(shù)據(jù)庫廠商的實(shí)現(xiàn)中,真正滿足ACID的事務(wù)很少。例如MySQL的NDB Cluster事務(wù)不滿足持久性;Oracle默認(rèn)的事務(wù)隔離級別為READ COMMITTED,不滿足隔離性;InnoDB默認(rèn)事務(wù)隔離級別是可重復(fù)讀,完全滿足ACID的特性。因此與其說ACID是事務(wù)必須滿足的條件,不如說它們是衡量事務(wù)的四個(gè)維度。
MySQL InnoDB 引擎的默認(rèn)隔離級別雖然是「可重復(fù)讀」,但是它很大程度上避免幻讀現(xiàn)象,解決的方案有兩種:
- 針對快照讀(普通 select 語句),是通過 MVCC 方式解決了不可重復(fù)讀和幻讀,因?yàn)榭芍貜?fù)讀隔離級別下,事務(wù)執(zhí)行過程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動時(shí)看到的數(shù)據(jù)是一致的,即使中途有其他事務(wù)插入了一條數(shù)據(jù),是查詢不出來這條數(shù)據(jù)的。MVVC在下面會仔細(xì)介紹。
Read Committed隔離級別:每次select都生成一個(gè)快照讀。 Read Repeatable隔離級別:開啟事務(wù)后第一個(gè)select語句才是快照讀的地方,而不是一開啟事務(wù)就快照讀。
- 針對當(dāng)前讀(select ... for update, delete, insert; select...lock in share mode (共享讀鎖) 等語句),是通過 next-key lock(行記錄鎖+間隙鎖)方式解決了幻讀,因?yàn)楫?dāng)執(zhí)行 select ... for update 語句的時(shí)候,會加上 next-key lock,如果有其他事務(wù)在 next-key lock 鎖范圍內(nèi)插入了一條記錄,那么這個(gè)插入語句就會被阻塞,無法成功插入,所以就很好了避免幻讀問題。對主鍵或唯一索引,如果select查詢時(shí)where條件全部精確命中(=或者in),這種場景本身就不會出現(xiàn)幻讀,所以只會加行記錄鎖。關(guān)于鎖這塊,后續(xù)有專門的章節(jié)進(jìn)行介紹。
總結(jié):事務(wù)的隔離性由MVCC和鎖來實(shí)現(xiàn),而原子性、一致性、持久性通過數(shù)據(jù)庫的redo和undo日志來完成。接下來會詳細(xì)介紹其實(shí)現(xiàn)原理。
MVVC如何實(shí)現(xiàn)事務(wù)的隔離?
MVCC,全稱Multi-Version Concurrency Control,即多版本并發(fā)控制。MVCC是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫管理系統(tǒng)中,實(shí)現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問。MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時(shí),也能做到不加鎖,非阻塞并發(fā)讀。
MVVC是一種用來解決讀-寫沖突的無鎖并發(fā)控制,簡單總結(jié)就是為事務(wù)分配單向增長的時(shí)間戳,為每個(gè)修改保存一個(gè)版本,版本與事務(wù)時(shí)間戳關(guān)聯(lián),讀操作只讀該事務(wù)開始前的數(shù)據(jù)庫的快照。 所以MVCC可以為數(shù)據(jù)庫解決以下問題:在并發(fā)讀寫數(shù)據(jù)庫時(shí),可以做到在讀操作時(shí)不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數(shù)據(jù)庫并發(fā)讀寫的性能;同時(shí)還可以解決臟讀,幻讀,不可重復(fù)讀等事務(wù)隔離問題,但不能解決更新丟失問題。
MVVC的實(shí)現(xiàn),依賴4個(gè)隱式字段,undo日志 ,Read View 來實(shí)現(xiàn)的。
隱式字段
每行記錄除了我們自定義的字段外,還有數(shù)據(jù)庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
- DB_ROW_ID 6byte, 隱含的自增ID(隱藏主鍵),如果數(shù)據(jù)表沒有主鍵,InnoDB會自動以DB_ROW_ID產(chǎn)生一個(gè)聚簇索引
- DB_TRX_ID 6byte, 最近修改(修改/插入)事務(wù)ID:記錄創(chuàng)建這條記錄/最后一次修改該記錄的事務(wù)ID
- DB_ROLL_PTR 7byte, 回滾指針,指向這條記錄的上一個(gè)版本(存儲于rollback segment里)
- DELETED_BIT 1byte, 記錄被更新或刪除并不代表真的刪除,而是刪除flag變了。
如上圖,DB_ROW_ID是數(shù)據(jù)庫默認(rèn)為該行記錄生成的唯一隱式主鍵;DB_TRX_ID是當(dāng)前操作該記錄的事務(wù)ID; 而DB_ROLL_PTR是一個(gè)回滾指針,用于配合undo日志,指向上一個(gè)舊版本;delete flag沒有展示出來。
undo log
InnoDB把這些為了回滾而記錄的這些東西稱之為undo log。這里需要注意的一點(diǎn)是,由于查詢操作(SELECT)并不會修改任何用戶記錄,所以在查詢操作執(zhí)行時(shí),并不需要記錄相應(yīng)的undo log。undo log主要分為3種:
- Insert undo log :插入一條記錄時(shí),至少要把這條記錄的主鍵值記下來,之后回滾的時(shí)候只需要把這個(gè)主鍵值對應(yīng)的記錄刪掉就好了。
- Update undo log:修改一條記錄時(shí),至少要把修改這條記錄前的舊值都記錄下來,這樣之后回滾時(shí)再把這條記錄更新為舊值就好了。
- Delete undo log:刪除一條記錄時(shí),至少要把這條記錄中的內(nèi)容都記下來,這樣之后回滾時(shí)再把由這些內(nèi)容組成的記錄插入到表中就好了。刪除操作都只是設(shè)置一下老記錄的DELETED_BIT,并不真正將過時(shí)的記錄刪除。
這里舉一個(gè)例子,比如我們想更新Person表中的數(shù)據(jù),有兩個(gè)事務(wù)先后對同一行數(shù)據(jù)進(jìn)行了修改,那么undo log中,不會僅僅只保存最近修改的舊版本記錄,而是通過鏈表的方式將不同版本連接起來。在下面的例子中,
- Person表中有一行數(shù)據(jù),name為Jerry,age是24歲。
- 事務(wù)A將name修改為Tom,數(shù)據(jù)修改完成之后,會把舊記錄拷貝到undo log中,并將隱藏字段的事務(wù)ID修改為當(dāng)前事務(wù)ID,這里假設(shè)從1開始,回滾指針指向undo log的副本記錄,說明上一個(gè)版本就是它。
- 事務(wù)B將年齡修改為30,相同的方式,A事務(wù)修改過后的記錄會被放到undo log,而事務(wù)B會把事務(wù)ID修改為2,同時(shí)回滾指針指向undo log中A事務(wù)修改過后的數(shù)據(jù)。
- 最后的形成的回滾鏈路如下。
ReadView
在上面介紹undo log的時(shí)候可以看到,undo log中維護(hù)了每條數(shù)據(jù)的多個(gè)版本,如果新來的一個(gè)事務(wù)也訪問這同一條數(shù)據(jù),如何判斷該讀取這條數(shù)據(jù)的哪個(gè)版本呢?此時(shí)就需要ReadView來做多版本的并發(fā)控制,根據(jù)查詢的時(shí)機(jī)來選擇一個(gè)當(dāng)前事務(wù)可見的舊版本數(shù)據(jù)讀取。
當(dāng)一個(gè)事務(wù)啟動后,首次執(zhí)行select操作時(shí),MVCC就會生成一個(gè)數(shù)據(jù)庫當(dāng)前的ReadView,通常而言,一個(gè)事務(wù)與一個(gè)ReadView屬于一對一的關(guān)系(不同隔離級別下也會存在細(xì)微差異),ReadView一般包含四個(gè)核心內(nèi)容:
- creator_trx_id:代表創(chuàng)建當(dāng)前這個(gè)ReadView的事務(wù)ID。
- trx_ids:表示在生成當(dāng)前ReadView時(shí),系統(tǒng)內(nèi)活躍的事務(wù)ID列表。
- up_limit_id:活躍的事務(wù)列表中,最小的事務(wù)ID。
- low_limit_id:表示在生成當(dāng)前ReadView時(shí),系統(tǒng)中要給下一個(gè)事務(wù)分配的ID值。
可以通過如下的示意圖進(jìn)一步理解ReadView,
假設(shè)目前數(shù)據(jù)庫中共有T1~T5這五個(gè)事務(wù),T1、T2、T4還在執(zhí)行,T3已經(jīng)回滾,T5已經(jīng)提交,此時(shí)當(dāng)有一條查詢語句執(zhí)行時(shí),就會利用MVCC機(jī)制生成一個(gè)ReadView,由于前面講過,單純由一條select語句組成的事務(wù)并不會分配事務(wù)ID,因此默認(rèn)為0,所以目前這個(gè)快照的信息如下:
{ "creator_trx_id" : "0", "trx_ids" : "[1,2,4]", "up_limit_id" : "1", "low_limit_id" : "6" }
當(dāng)我們拿到ReadView之后,如何判斷當(dāng)前的事務(wù)能夠看到哪些版本的數(shù)據(jù),這里會遵循一個(gè)可見性算法,簡單來講就是將要被修改數(shù)據(jù)的最新記錄的DB_TRX_ID(即當(dāng)前事務(wù)ID),與ReadView維護(hù)的其他事務(wù)ID進(jìn)行比較,來確定當(dāng)前事務(wù)能看到的最新老版本。
這里結(jié)合MySQL的算法實(shí)現(xiàn)來看,下面是MySQL 8.1里面關(guān)于這個(gè)可見性算法的實(shí)現(xiàn)??梢钥吹剑w流程如下:
- 首先判斷
DB_TRX_ID < up_limit_id
,此時(shí)說明該事務(wù)已經(jīng)結(jié)束,所以DB_TRX_ID對應(yīng)的舊版本對ReadView可見。如果DB_TRX_ID = creator_trx_id
,說明ReadView是當(dāng)前事務(wù)中生成的,當(dāng)然可以看到自己的修改,所以也是可見的。 - 接著判斷
DB_TRX_ID >= low_limit_id
,則代表DB_TRX_ID 所在的記錄在Read View生成后才出現(xiàn)的,那對當(dāng)前事務(wù)肯定不可見。但是如果DB_TRX_ID < low_limit_id
,并且當(dāng)前無活躍的事務(wù)id,說明所有事務(wù)已經(jīng)提交了,因此該條記錄也是可見的。 - 判斷DB_TRX_ID 是否在活躍事務(wù)之中。如果在,則代表Read View生成時(shí)刻,這個(gè)事務(wù)還在活躍,還沒有Commit,因此這個(gè)事務(wù)修改的數(shù)據(jù),我當(dāng)前事務(wù)也是看不見的;如果不在,則說明,你這個(gè)事務(wù)在Read View生成之前就已經(jīng)Commit了,你修改的結(jié)果,我當(dāng)前事務(wù)是能看見的。
// https://dev.mysql.com/doc/dev/mysql-server/latest/read0types_8h_source.html /** Check whether the changes by id are visible. @param[in] id transaction id to check against the view @param[in] name table name @return whether the view sees the modifications of id. */ [[nodiscard]] bool changes_visible(trx_id_t id, const table_name_t &name) const { ut_ad(id > 0); if (id < m_up_limit_id || id == m_creator_trx_id) { return (true); } check_trx_id_sanity(id, name); if (id >= m_low_limit_id) { return (false); } else if (m_ids.empty()) { return (true); } const ids_t::value_type *p = m_ids.data(); return (!std::binary_search(p, p + m_ids.size(), id)); }
MVCC原理總結(jié)
MVCC主要由下面兩個(gè)核心功能組成,undo log實(shí)現(xiàn)數(shù)據(jù)的多版本,ReadView實(shí)現(xiàn)多版本的并發(fā)控制。
- 當(dāng)一個(gè)事務(wù)嘗試改動某條數(shù)據(jù)時(shí),會將原本表中的舊數(shù)據(jù)放入undo log中。
- 當(dāng)一個(gè)事務(wù)嘗試查詢某條數(shù)據(jù)時(shí),MVCC會生成一個(gè)ReadView快照。
這里舉一個(gè)例子回顧下整個(gè)流程:
假設(shè)有A和B兩個(gè)并發(fā)事務(wù),其中事務(wù)A在修改第一行的數(shù)據(jù),而事務(wù)B準(zhǔn)備讀取這條數(shù)據(jù),那么B在具體執(zhí)行過程中,當(dāng)出現(xiàn)SELECT語句時(shí),會根據(jù)MySQL的當(dāng)前情況生成一個(gè)ReadView。
- 判斷數(shù)據(jù)行中的隱藏列TRX_ID與ReadView中的creator_trx_id是否相同,如果相同表示是同一個(gè)事務(wù),數(shù)據(jù)可見。
- 判斷TRX_ID是否小于up_limit_id,也就是最小活躍事務(wù)ID,如果小的話,說明改動這行數(shù)據(jù)的事務(wù)在ReadView生成之前就結(jié)束了,所以是可見的;如果大于的話,繼續(xù)往下走。
- 判斷TRX_ID是否小于low_limit_id,也就是當(dāng)前ReadView生成時(shí),下一個(gè)會分配的事務(wù)ID。如果大于或等于low_limit_id,說明修改該數(shù)據(jù)的事務(wù)是生成ReadView之后才開啟的,當(dāng)然是不可見的。如果小于low_limit_id,則進(jìn)行下一步判斷。
- 如果TRX_ID在trx_ids中,說明該數(shù)據(jù)行對應(yīng)的事務(wù)還在執(zhí)行,因此對于當(dāng)前事務(wù)而言,該數(shù)據(jù)不可見;如果TRX_ID不在trx_ids中,說明該事務(wù)在生成ReadView時(shí)已經(jīng)結(jié)束,因此是可見的。
如果undo log中存在某行數(shù)據(jù)的多個(gè)版本,那么在實(shí)際中會根據(jù)隱藏列roll_ptr依次遍歷整個(gè)鏈表,按照上面的流程,找到第一條滿足條件的數(shù)據(jù)并返回。
RC、RR不同級別下的MVVC機(jī)制
ReadView
是一個(gè)事務(wù)中只生成一次,還是每次select
時(shí)都會生成呢?這個(gè)問題和MySQL的事務(wù)隔離機(jī)制有關(guān),RC和RR下的實(shí)現(xiàn)有些許不同。
- RC(讀已提交):每個(gè)快照讀都會生成并獲取最新的Read View,保證已經(jīng)提交事務(wù)的修改對當(dāng)前事務(wù)可見。
- RR(可重復(fù)讀):同一個(gè)事務(wù)中的第一個(gè)快照讀才會創(chuàng)建Read View, 之后的快照讀獲取的都是使用同一個(gè)Read View;這樣整個(gè)事務(wù)期間讀到的記錄都是事務(wù)啟動前的記錄。
undo log和redo log在事務(wù)里面有什么用?
上面介紹了事務(wù)隔離性的實(shí)現(xiàn)原理,即通過多版本并發(fā)控制(MVCC,Multiversion Concurrency Control )解決不可重復(fù)讀問題,加上間隙鎖(也就是并發(fā)控制)解決幻讀問題。保證了較好的并發(fā)性能。
而事務(wù)的原子性、一致性和持久性則是通過事務(wù)日志實(shí)現(xiàn),主要就是redo log和undo log。了解完下面這些內(nèi)容,那就明白了其中的原理和實(shí)現(xiàn)。
1. redo log
為什么需要redo log
在 MySQL 中,如果每一次的更新要寫進(jìn)磁盤,這么做會帶來嚴(yán)重的性能問題:
- 因?yàn)?Innodb 是以頁為單位進(jìn)行磁盤交互的,而一個(gè)事務(wù)很可能只修改一個(gè)數(shù)據(jù)頁里面的幾個(gè)字節(jié),這時(shí)將完整的數(shù)據(jù)頁刷到磁盤的話,太浪費(fèi)資源了。
- 一個(gè)事務(wù)可能涉及修改多個(gè)數(shù)據(jù)頁,并且這些數(shù)據(jù)頁在物理上并不連續(xù),使用隨機(jī) IO 寫入性能太差。
因此每當(dāng)有一條新的數(shù)據(jù)需要更新時(shí),InnoDB 引擎就會先更新內(nèi)存(同時(shí)標(biāo)記為臟頁),然后將本次對這個(gè)頁的修改以 redo log 的形式記錄下來,這個(gè)時(shí)候更新就算完成了。之后,InnoDB 引擎會在適當(dāng)?shù)臅r(shí)候,由后臺線程將緩存在 Buffer Pool 的臟頁刷新到磁盤里,這就是 WAL (Write-Ahead Logging)技術(shù)。
WAL 技術(shù)指的是, MySQL 的寫操作并不是立刻寫到磁盤上,而是先寫日志,然后在合適的時(shí)間再寫到磁盤上。
整個(gè)過程如下:
什么是redo log
redo log 是物理日志,記錄了某個(gè)數(shù)據(jù)頁做了什么修改,比如對A表空間中的B數(shù)據(jù)頁C偏移量的地方做了D更新,每當(dāng)執(zhí)行一個(gè)事務(wù)就會產(chǎn)生這樣的一條或者多條物理日志。
在事務(wù)提交時(shí),只要先將 redo log 持久化到磁盤即可,可以不需要等到將緩存在 Buffer Pool 里的臟頁數(shù)據(jù)持久化到磁盤。當(dāng)系統(tǒng)崩潰時(shí),雖然臟頁數(shù)據(jù)沒有持久化,但是 redo log 已經(jīng)持久化,接著 MySQL 重啟后,可以根據(jù) redo log 的內(nèi)容,將所有數(shù)據(jù)恢復(fù)到最新的狀態(tài)。
redo log有什么好處
總結(jié)來看,有一下兩點(diǎn):
- 將寫數(shù)據(jù)的操作,由隨機(jī)寫變成了順序?qū)?/strong>。在寫入redo log時(shí),使用的是追加操作,所以對應(yīng)磁盤是順序?qū)?。而直接寫?shù)據(jù),需要先找到數(shù)據(jù)的位置,然后才能寫磁盤,所以磁盤操作是隨機(jī)寫。因此直接寫入redo log比直接寫入磁盤效率高很多。
- 實(shí)現(xiàn)事務(wù)的持久性。 使用redo log之后,雖然每次修改數(shù)據(jù)之后,數(shù)據(jù)處于緩沖中,如果MySQL重啟,緩沖中的數(shù)據(jù)會丟失,但是我們可以根據(jù)redo log的內(nèi)容將數(shù)據(jù)恢復(fù)到最新的狀態(tài);保證了事務(wù)修改的數(shù)據(jù),不會丟失,也就是實(shí)現(xiàn)了持久性。
redo log如何寫入磁盤?
redo log并不是每次寫入都會刷新到數(shù)據(jù)頁,而是采取一定的策略周期性的刷寫到磁盤上。所以,它其實(shí)包括了兩部分,分別是內(nèi)存中的日志緩沖(redo log buffer)和磁盤上的日志文件(redo log file)。
由于MySQL處于用戶空間,而用戶空間下的緩沖區(qū)數(shù)據(jù)是無法直接寫入磁盤的,因?yàn)橹虚g必須經(jīng)過操作系統(tǒng)的內(nèi)核空間緩沖區(qū)(OS Buffer)。所以,redo log buffer 寫入 redo logfile 實(shí)際上是先寫入 OS Buffer,然后操作系統(tǒng)調(diào)用 fsync() 函數(shù)將日志刷到磁盤。過程如下:
MySQL支持用戶自定義在commit時(shí)如何將log buffer中的日志刷log file中。這種控制通過變量 innodb_flush_log_at_trx_commit 的值來決定。該變量有3種值:0、1、2,默認(rèn)為1。但注意,這個(gè)變量只是控制commit動作是否刷新log buffer到磁盤。
參數(shù)值 | 含義 |
---|---|
0(延遲寫) | 事務(wù)提交時(shí)不會將 redo log buffer 中日志寫到 os buffer,而是每秒寫入os buffer 并調(diào)用 fsync() 寫入到 redo logfile 中。也就是說設(shè)置為 0 時(shí)是(大約)每秒刷新寫入到磁盤中的,當(dāng)系統(tǒng)崩潰,會丟失1秒鐘的數(shù)據(jù)。 |
1(實(shí)時(shí)寫、實(shí)時(shí)刷新) | 事務(wù)每次提交都會將 redo log buffer 中的日志寫入 os buffer 并調(diào)用 fsync() 刷到 redo logfile 中。這種方式即使系統(tǒng)崩潰也不會丟失任何數(shù)據(jù),但是因?yàn)槊看翁峤欢紝懭氪疟P,IO的性能差。 |
2(實(shí)時(shí)寫、延遲刷新) | 每次提交都僅寫入到 os buffer,然后是每秒調(diào)用 fsync() 將 os buffer 中的日志寫入到 redo log file。 |
三種方案總結(jié)如下:
- 針對參數(shù) 0 :會把緩存在 redo log buffer 中的 redo log ,通過調(diào)用
write()
寫到系統(tǒng)緩存,然后調(diào)用fsync()
持久化到磁盤。所以參數(shù)為 0 的策略,MySQL 進(jìn)程的崩潰會導(dǎo)致上一秒鐘所有事務(wù)數(shù)據(jù)的丟失; - 針對參數(shù) 2 :調(diào)用 fsync,將緩存在系統(tǒng)緩存里的 redo log 持久化到磁盤。所以參數(shù)為 2 的策略,較取值為 0 情況下更安全,因?yàn)?MySQL 進(jìn)程的崩潰并不會丟失數(shù)據(jù),只有在操作系統(tǒng)崩潰或者系統(tǒng)斷電的情況下,上一秒鐘所有事務(wù)數(shù)據(jù)才可能丟失。
在主從復(fù)制結(jié)構(gòu)中,要保證事務(wù)的持久性和一致性,需要對日志相關(guān)變量設(shè)置為如下:
如果啟用了二進(jìn)制日志,則設(shè)置sync_binlog=1,即每提交一次事務(wù)同步寫到磁盤中。
總是設(shè)置innodb_flush_log_at_trx_commit=1,即每提交一次事務(wù)都寫到磁盤中。 上述兩項(xiàng)變量的設(shè)置保證了:每次提交事務(wù)都寫入二進(jìn)制日志和事務(wù)日志,并在提交時(shí)將它們刷新到磁盤中。
redo log file結(jié)構(gòu)是怎么樣的?
InnoDB 的 redo log 是固定大小的。比如可以配置為一組 4 個(gè)文件,每個(gè)文件的大小是 1GB,那么 redo log file 可以記錄 4GB 的操作。從頭開始寫。寫到末尾又回到開頭循環(huán)寫。如下圖:
上圖中,write pos 表示 redo log 當(dāng)前記錄的 LSN (邏輯序列號) 位置,一邊寫一遍后移,寫到第 3 號文件末尾后就回到 0 號文件開頭; check point 表示數(shù)據(jù)頁更改記錄刷盤后對應(yīng) redo log 所處的 LSN(邏輯序列號) 位置,也是往后推移并且循環(huán)的。
write pos 到 check point 之間的部分是 redo log 的未寫區(qū)域,可用于記錄新的記錄;check point 到 write pos 之間是 redo log 已寫區(qū)域,是待刷盤的數(shù)據(jù)頁更改記錄。
當(dāng) write pos 追上 check point 時(shí),表示 redo log file 寫滿了,這時(shí)候有就不能執(zhí)行新的更新。得停下來先擦除一些記錄(擦除前要先把記錄刷盤),再推動 check point 向前移動,騰出位置再記錄新的日志。
2. undo log
undo log有兩個(gè)作用:提供回滾和多個(gè)行版本控制(MVCC)。
在數(shù)據(jù)修改的時(shí)候,不僅記錄了redo,還記錄了相對應(yīng)的undo,如果因?yàn)槟承┰驅(qū)е率聞?wù)失敗或回滾了,可以借助該undo進(jìn)行回滾。
undo log和redo log記錄物理日志不一樣,它是邏輯日志??梢哉J(rèn)為當(dāng)delete一條記錄時(shí),undo log中會記錄一條對應(yīng)的insert記錄,反之亦然,當(dāng)update一條記錄時(shí),它記錄一條對應(yīng)相反的update記錄。
當(dāng)執(zhí)行rollback時(shí),就可以從undo log中的邏輯記錄讀取到相應(yīng)的內(nèi)容并進(jìn)行回滾。有時(shí)候應(yīng)用到行版本控制的時(shí)候,也是通過undo log來實(shí)現(xiàn)的:當(dāng)讀取的某一行被其他事務(wù)鎖定時(shí),它可以從undo log中分析出該行記錄以前的數(shù)據(jù)是什么,從而提供該行版本信息,讓用戶實(shí)現(xiàn)非鎖定一致性讀取。
undo log 和數(shù)據(jù)頁的刷盤策略是一樣的,都需要通過 redo log 保證持久化。 buffer pool 中有 undo 頁,對 undo 頁的修改也都會記錄到 redo log。redo log 會每秒刷盤,提交事務(wù)時(shí)也會刷盤,數(shù)據(jù)頁和 undo 頁都是靠這個(gè)機(jī)制保證持久化的。
總結(jié)回顧
InnoDB通過MVVC、undo log和redo log實(shí)現(xiàn)了事務(wù)的ACID特性,
- MVCC 是通過 ReadView + undo log 實(shí)現(xiàn)的。undo log 為每條記錄保存多份歷史數(shù)據(jù),MySQL 在執(zhí)行快照讀(普通 select 語句)的時(shí)候,會根據(jù)事務(wù)的 Read View 里的信息,順著 undo log 的版本鏈找到滿足其可見性的記錄。實(shí)現(xiàn)了事務(wù)的隔離性。
- undo log記錄了每行數(shù)據(jù)的歷史版本,當(dāng)現(xiàn)了錯(cuò)誤或者用戶執(zhí) 行了 ROLLBACK 語句,MySQL 可以利用 undo log 中的歷史數(shù)據(jù)將數(shù)據(jù)恢復(fù)到事務(wù)開始之前的狀態(tài)。保證了事務(wù)的一致性和原子性。
- 使用redo log之后,雖然每次修改數(shù)據(jù)之后,數(shù)據(jù)處于緩沖中,如果MySQL重啟,緩沖中的數(shù)據(jù)會丟失,但是我們可以根據(jù)redo log的內(nèi)容將數(shù)據(jù)恢復(fù)到最新的狀態(tài);保證了事務(wù)修改的數(shù)據(jù),不會丟失,也就是實(shí)現(xiàn)了事務(wù)的持久性。
以上就是詳解MySQL事務(wù)的ACID如何實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于MySQL實(shí)現(xiàn)事務(wù)的ACID的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MySQL數(shù)據(jù)庫子查詢?sub?query
這篇文章主要介紹了MySQL數(shù)據(jù)庫子查詢?sub?query,子查詢指嵌套查詢下層的程序模塊,當(dāng)一個(gè)查詢是另一個(gè)查詢的條件的時(shí)候,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下下面文章內(nèi)容介紹2022-06-06MySQL創(chuàng)建和刪除表操作命令實(shí)例講解
這篇文章主要介紹了MySQL創(chuàng)建和刪除表操作命令實(shí)例講解,本文講解了創(chuàng)建表、創(chuàng)建臨時(shí)表、查看已經(jīng)創(chuàng)建的mysql表等內(nèi)容,需要的朋友可以參考下2014-12-12獲取MySQL數(shù)據(jù)表列信息的三種方法實(shí)現(xiàn)
本文介紹了獲取MySQL數(shù)據(jù)表列信息的三種方法實(shí)現(xiàn),包含SHOWCOLUMNS命令、DESCRIBE命令以及查詢INFORMATION_SCHEMA.COLUMNS表,具有一定的參考價(jià)值,感興趣的可以了解一下2024-12-12mysql中distinct和group?by的區(qū)別淺析
distinct簡單來說就是用來去重的,而group by的設(shè)計(jì)目的則是用來聚合統(tǒng)計(jì)的,兩者在能夠?qū)崿F(xiàn)的功能上有些相同之處,但應(yīng)該仔細(xì)區(qū)分,下面這篇文章主要給大家介紹了關(guān)于mysql中distinct和group?by區(qū)別的相關(guān)資料,需要的朋友可以參考下2023-05-05