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