大家好,我是【碼老思】,事務是一個資料庫繞不開的話題,今天和大家一起聊聊。
事務(Transaction)是並行控制的基本單位。所謂的事務呢,它是一個操作序列,這些操作要麼都執行,要麼都不執行,它是一個不可分割的工作單位。
在介紹事務的特性之前,我們先看下MySQL的邏輯架構,
如上圖所示,MySQL伺服器邏輯架構從上往下可以分為三層:
後續討論主要以InnoDB為主。
事務的特性,可以總結為如下4個方面:
原子性(Atomicity):原子性是指整個資料庫的事務是一個不可分割的工作單位,在每一個都應該是原子操作。當我們執行一個事務的時候,如果在一系列的操作中,有一個操作失敗了,那麼需要將這一個事務中的所有操作恢復到執行事務之前的狀態,這就是事務的原子性。
一致性(Consistency): 一致性呢是指事務將資料庫從一種狀態轉變成為下一種一致性的狀態,也就是說是在事務的執行前後,這兩種狀態應該是一樣的,也就是在資料庫的完整性約束不會被破壞。另外的話,還需要注意的是一致性不關注中間的過程是發生了什麼。
隔離性(lsolation): Mysql資料庫可以同時的話啟動很多的事務,但是呢,事務跟事務之間他們是相互分離的,也就是互不影響的,這就是事務的隔離性。下面有介紹事務的四大隔離級別。
永續性(Durability): 事務的永續性是指事務一旦提交,就是永久的了。說白了就是發生了問題,資料庫也是可以恢復的。因此永續性保證事務的高可靠性。
談到事務的四大特性,不得不說一下MySQL事務的隔離機制,在不同的資料庫連線中,一個連線的事務並不會影響其他連線,這是基於事務隔離機制實現的。在MySQL
中,事務隔離機制分為了四個級別:
Read uncommitted / RU:讀未提交,就是一個事務可以讀取另一個未提交事務的資料。毫無疑問,這樣會造成大量的髒讀,所以資料庫一般不會採用這種隔離級別。
Read committed / RC:讀已提交,就是一個事務讀到的資料必須是其他事務已經提交的資料,這樣就避免了髒讀的情況。但是如果有兩個並行的事務A和B,處理同一批的資料,如果事務A在這個過程中,修改了資料並提交;那麼在事務B中可能前後看到兩個不一樣的資料,這就造成不可重複讀的情況。
Repeatable read / RR:可重複讀,就是在開始讀取資料(事務開啟)時,不再允許修改操作。這樣就解決了不可重複讀的問題,但是需要注意的是,不可重複讀對應的是修改,即UPDATE操作。但是可能還會有幻讀問題。因為幻讀問題對應的是插入INSERT操作,而不是UPDATE操作。
Serializable:序列化/序列化。它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的資料行上加上共用鎖。這種情況下所有事務序列執行,可以避免上面的出現的各種問題,但是在大並行場景下會導致大量的超時現象和鎖競爭,所以一般也很少採用。
上述四個級別,越靠後並行控制度越高,也就是在多執行緒並行操作的情況下,出現問題的機率越小,但對應的也效能越差,MySQL
的事務隔離級別,預設為第三級別:Repeatable read可重複讀。
按照嚴格的標準,只有同時滿足ACID特性才是事務;但是目前各巨量資料庫廠商的實現中,真正滿足ACID的事務很少。例如MySQL的NDB Cluster事務不滿足永續性;Oracle預設的事務隔離級別為READ COMMITTED,不滿足隔離性;InnoDB預設事務隔離級別是可重複讀,完全滿足ACID的特性。因此與其說ACID是事務必須滿足的條件,不如說它們是衡量事務的四個維度。
**MySQL InnoDB 引擎的預設隔離級別雖然是「可重複讀」,但是它很大程度上避免幻讀現象,解決的方案有兩種:
Read Committed隔離級別:每次select都生成一個快照讀。
Read Repeatable隔離級別:開啟事務後第一個select語句才是快照讀的地方,而不是一開啟事務就快照讀。
總結:事務的隔離性由MVCC和鎖來實現,而原子性、一致性、永續性通過資料庫的redo和undo紀錄檔來完成。接下來會詳細介紹其實現原理。
MVCC,全稱Multi-Version Concurrency Control,即多版本並行控制。MVCC是一種並行控制的方法,一般在資料庫管理系統中,實現對資料庫的並行存取。MVCC在MySQL InnoDB中的實現主要是為了提高資料庫並行效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞並行讀。
MVVC是一種用來解決讀-寫衝突的無鎖並行控制,簡單總結就是為事務分配單向增長的時間戳,為每個修改儲存一個版本,版本與事務時間戳關聯,讀操作唯讀該事務開始前的資料庫的快照。 所以MVCC可以為資料庫解決以下問題:在並行讀寫資料庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了資料庫並行讀寫的效能;同時還可以解決髒讀,幻讀,不可重複讀等事務隔離問題,但不能解決更新丟失問題。
MVVC的實現,依賴4個隱式欄位,undo紀錄檔 ,Read View 來實現的。
每行記錄除了我們自定義的欄位外,還有資料庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等欄位
如上圖,DB_ROW_ID是資料庫預設為該行記錄生成的唯一隱式主鍵;DB_TRX_ID是當前操作該記錄的事務ID; 而DB_ROLL_PTR是一個回滾指標,用於配合undo紀錄檔,指向上一個舊版本;delete flag沒有展示出來。
InnoDB把這些為了回滾而記錄的這些東西稱之為undo log。這裡需要注意的一點是,由於查詢操作(SELECT)並不會修改任何使用者記錄,所以在查詢操作執行時,並不需要記錄相應的undo log。undo log主要分為3種:
這裡舉一個例子,比如我們想更新Person表中的資料,有兩個事務先後對同一行資料進行了修改,那麼undo log中,不會僅僅只儲存最近修改的舊版本記錄,而是通過連結串列的方式將不同版本連線起來。在下面的例子中,
在上面介紹undo log的時候可以看到,undo log中維護了每條資料的多個版本,如果新來的一個事務也存取這同一條資料,如何判斷該讀取這條資料的哪個版本呢?此時就需要ReadView來做多版本的並行控制,根據查詢的時機來選擇一個當前事務可見的舊版本資料讀取。
當一個事務啟動後,首次執行select操作時,MVCC就會生成一個資料庫當前的ReadView,通常而言,一個事務與一個ReadView屬於一對一的關係(不同隔離級別下也會存在細微差異),ReadView一般包含四個核心內容:
可以通過如下的示意圖進一步理解ReadView,
假設目前資料庫中共有T1~T5這五個事務,T1、T2、T4還在執行,T3已經回滾,T5已經提交,此時當有一條查詢語句執行時,就會利用MVCC機制生成一個ReadView,由於前面講過,單純由一條select語句組成的事務並不會分配事務ID,因此預設為0,所以目前這個快照的資訊如下:
{ "creator_trx_id" : "0", "trx_ids" : "[1,2,4]", "up_limit_id" : "1", "low_limit_id" : "6" }
當我們拿到ReadView之後,如何判斷當前的事務能夠看到哪些版本的資料,這裡會遵循一個可見性演演算法,簡單來講就是將要被修改資料的最新記錄的DB_TRX_ID(即當前事務ID),與ReadView維護的其他事務ID進行比較,來確定當前事務能看到的最新老版本。
這裡結合MySQL的演演算法實現來看,下面是MySQL 8.1裡面關於這個可見性演演算法的實現。可以看到,整體流程如下:
DB_TRX_ID < up_limit_id
,此時說明該事務已經結束,所以DB_TRX_ID對應的舊版本對ReadView可見。如果 DB_TRX_ID = creator_trx_id
,說明ReadView是當前事務中生成的,當然可以看到自己的修改,所以也是可見的。DB_TRX_ID >= low_limit_id
,則代表DB_TRX_ID 所在的記錄在Read View生成後才出現的,那對當前事務肯定不可見。但是如果DB_TRX_ID < low_limit_id
,並且當前無活躍的事務id,說明所有事務已經提交了,因此該條記錄也是可見的。// 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主要由下面兩個核心功能組成,undo log實現資料的多版本,ReadView實現多版本的並行控制。
這裡舉一個例子回顧下整個流程:
假設有A和B兩個並行事務,其中事務A在修改第一行的資料,而事務B準備讀取這條資料,那麼B在具體執行過程中,當出現SELECT語句時,會根據MySQL的當前情況生成一個ReadView。
如果undo log中存在某行資料的多個版本,那麼在實際中會根據隱藏列roll_ptr依次遍歷整個連結串列,按照上面的流程,找到第一條滿足條件的資料並返回。
ReadView
是一個事務中只生成一次,還是每次select
時都會生成呢?這個問題和MySQL的事務隔離機制有關,RC和RR下的實現有些許不同。
上面介紹了事務隔離性的實現原理,即通過多版本並行控制(MVCC,Multiversion Concurrency Control )解決不可重複讀問題,加上間隙鎖(也就是並行控制)解決幻讀問題。保證了較好的並行效能。
而事務的原子性、一致性和永續性則是通過事務紀錄檔實現,主要就是redo log和undo log。瞭解完下面這些內容,那就明白了其中的原理和實現。
為什麼需要redo log
在 MySQL 中,如果每一次的更新要寫進磁碟,這麼做會帶來嚴重的效能問題:
因此每當有一條新的資料需要更新時,InnoDB 引擎就會先更新記憶體(同時標記為髒頁),然後將本次對這個頁的修改以 redo log 的形式記錄下來,這個時候更新就算完成了。之後,InnoDB 引擎會在適當的時候,由後臺執行緒將快取在 Buffer Pool 的髒頁重新整理到磁碟裡,這就是 WAL (Write-Ahead Logging)技術。
WAL 技術指的是, MySQL 的寫操作並不是立刻寫到磁碟上,而是先寫紀錄檔,然後在合適的時間再寫到磁碟上。
整個過程如下:
什麼是redo log
redo log 是物理紀錄檔,記錄了某個資料頁做了什麼修改,比如對A表空間中的B資料頁C偏移量的地方做了D更新,每當執行一個事務就會產生這樣的一條或者多條物理紀錄檔。
在事務提交時,只要先將 redo log 持久化到磁碟即可,可以不需要等到將快取在 Buffer Pool 裡的髒頁資料持久化到磁碟。當系統崩潰時,雖然髒頁資料沒有持久化,但是 redo log 已經持久化,接著 MySQL 重啟後,可以根據 redo log 的內容,將所有資料恢復到最新的狀態。
redo log有什麼好處
總結來看,有一下兩點:
redo log如何寫入磁碟?
redo log並不是每次寫入都會重新整理到資料頁,而是採取一定的策略週期性的刷寫到磁碟上。所以,它其實包括了兩部分,分別是記憶體中的紀錄檔緩衝(redo log buffer)和磁碟上的紀錄檔檔案(redo log file)。
由於MySQL處於使用者空間,而使用者空間下的緩衝區資料是無法直接寫入磁碟的,因為中間必須經過作業系統的核心空間緩衝區(OS Buffer)。所以,redo log buffer 寫入 redo logfile 實際上是先寫入 OS Buffer,然後作業系統呼叫 fsync() 函數將紀錄檔刷到磁碟。過程如下:
MySQL支援使用者自定義在commit時如何將log buffer中的紀錄檔刷log file中。這種控制通過變數 innodb_flush_log_at_trx_commit 的值來決定。該變數有3種值:0、1、2,預設為1。但注意,這個變數只是控制commit動作是否重新整理log buffer到磁碟。
引數值 | 含義 |
---|---|
0(延遲寫) | 事務提交時不會將 redo log buffer 中紀錄檔寫到 os buffer,而是每秒寫入os buffer 並呼叫 fsync() 寫入到 redo logfile 中。也就是說設定為 0 時是(大約)每秒重新整理寫入到磁碟中的,當系統崩潰,會丟失1秒鐘的資料。 |
1(實時寫、實時重新整理) | 事務每次提交都會將 redo log buffer 中的紀錄檔寫入 os buffer 並呼叫 fsync() 刷到 redo logfile 中。這種方式即使系統崩潰也不會丟失任何資料,但是因為每次提交都寫入磁碟,IO的效能差。 |
2(實時寫、延遲重新整理) | 每次提交都僅寫入到 os buffer,然後是每秒呼叫 fsync() 將 os buffer 中的紀錄檔寫入到 redo log file。 |
三種方案總結如下:
write()
寫到系統快取,然後呼叫 fsync()
持久化到磁碟。所以引數為 0 的策略,MySQL 程序的崩潰會導致上一秒鐘所有事務資料的丟失;在主從複製結構中,要保證事務的永續性和一致性,需要對紀錄檔相關變數設定為如下:
- 如果啟用了二進位制紀錄檔,則設定sync_binlog=1,即每提交一次事務同步寫到磁碟中。
- 總是設定innodb_flush_log_at_trx_commit=1,即每提交一次事務都寫到磁碟中。
上述兩項變數的設定保證了:每次提交事務都寫入二進位制紀錄檔和事務紀錄檔,並在提交時將它們重新整理到磁碟中。
redo log file結構是怎麼樣的?
InnoDB 的 redo log 是固定大小的。比如可以設定為一組 4 個檔案,每個檔案的大小是 1GB,那麼 redo log file 可以記錄 4GB 的操作。從頭開始寫。寫到末尾又回到開頭回圈寫。如下圖:
上圖中,write pos 表示 redo log 當前記錄的 LSN (邏輯序列號) 位置,一邊寫一遍後移,寫到第 3 號檔案末尾後就回到 0 號檔案開頭; check point 表示資料頁更改記錄刷盤後對應 redo log 所處的 LSN(邏輯序列號) 位置,也是往後推移並且迴圈的。
write pos 到 check point 之間的部分是 redo log 的未寫區域,可用於記錄新的記錄;check point 到 write pos 之間是 redo log 已寫區域,是待刷盤的資料頁更改記錄。
當 write pos 追上 check point 時,表示 redo log file 寫滿了,這時候有就不能執行新的更新。得停下來先擦除一些記錄(擦除前要先把記錄刷盤),再推動 check point 向前移動,騰出位置再記錄新的紀錄檔。
undo log有兩個作用:提供回滾和多個行版本控制(MVCC)。
在資料修改的時候,不僅記錄了redo,還記錄了相對應的undo,如果因為某些原因導致事務失敗或回滾了,可以藉助該undo進行回滾。
undo log和redo log記錄物理紀錄檔不一樣,它是邏輯紀錄檔。可以認為當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄。
當執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾。有時候應用到行版本控制的時候,也是通過undo log來實現的:當讀取的某一行被其他事務鎖定時,它可以從undo log中分析出該行記錄以前的資料是什麼,從而提供該行版本資訊,讓使用者實現非鎖定一致性讀取。
undo log 和資料頁的刷盤策略是一樣的,都需要通過 redo log 保證持久化。
buffer pool 中有 undo 頁,對 undo 頁的修改也都會記錄到 redo log。redo log 會每秒刷盤,提交事務時也會刷盤,資料頁和 undo 頁都是靠這個機制保證持久化的。
InnoDB通過MVVC、undo log和redo log實現了事務的ACID特性,
參考:
歡迎關注公眾號【碼老思】,第一時間獲取最通俗易懂的原創技術乾貨。