天天看點

mysql事務(包括redo log,undo log,MVCC)及事務實作原理

原子性:

一次事務中的所有操作,要麼全部完成要麼全部不執行。這裡是通過undo log來實作的。

undo log又是什麼呢,可以了解為要執行的sql的反向sql,也就是復原sql。譬如你insert了一條,undo log裡就會儲存一個delete xxx id = xxx。

持久性:

一旦一個事務被送出,就算伺服器崩潰,仍然不能丢資料,在下次啟動時需要能自動恢複。這裡是通過redo log實作的。

那麼如何才能持久呢,很簡單,寫入硬碟就行了。通過前面幾篇的學習,我們知道mysql大部分時間是在Innodb_buffer_pool裡做記憶體讀寫的,特定情況下才會落盤。這樣如果突然伺服器崩潰,沒來得及落盤的資料就會丢失。是以有了redo log順序寫磁盤(順序寫速度極快,後續的落盤是随機寫,速度慢),在事務送出後,事務日志會順序寫入磁盤,然後寫入pool記憶體裡。然後才是後續的那些按規則将索引資料落盤。

隔離性:

事務的隔離性是通過讀寫鎖+MVCC來實作的。mysql在為了并發量和資料隔離方面做了很多的嘗試,其中MVCC就是比較好的解決方案。

也就是面試最常見的4大隔離級别。

       1.讀未送出          其它事務未送出就可以讀

       2.讀已送出          其它事務隻有送出了才能讀

       3.可重複讀          隻管自己啟動事務時候的狀态,不受其它事務的影響(mysql預設)

       4.事務串行          按照順序送出事務保證了資料的安全性,但無法實作并發

一緻性:

即資料一緻性,是通過上面的三種加起來聯合實作的。

ACID隻是個概念,最終目的就是保證資料的一緻性和可靠不丢。

Undo Log

所謂的undo log就是復原日志,當進行插入、删除、修改操作時,一定會生成undo log,并且一定優先于修改後的資料落盤。

我直接借用别人的圖了,zhangsan的銀行賬戶有1000元,他要轉400元到理财賬戶。

mysql事務(包括redo log,undo log,MVCC)及事務實作原理
mysql事務(包括redo log,undo log,MVCC)及事務實作原理

可以看到,每一條變更資料的操作,都伴随一條undo log的生成。undo log就是記錄資料的原始狀态。這一點在一些其他的分布式事務的架構裡也有所使用,譬如seata就是采用自己解析sql并生成反向sql存儲下來,将來抛異常時就執行復原語句的方式來做的分布式事務,而不借助于mysql自身的undo log機制。

有了undo log,如果事務執行失敗要復原,那很簡單,直接将undo log裡的復原語句執行一遍就好了。mysql就是通過這種方式完成的原子性。

Redo Log

redo log是完成資料持久性的,事務一旦送出,其所做的修改就會永久儲存到資料庫中,而不能丢失,即便是mysql伺服器挂掉了也不能丢。

mysql資料是存儲到磁盤的,但讀寫其實都是操作的buffer pool緩存(前面幾篇講過insert buffer)。這樣就會造成寫入時,如果僅僅寫入到buffer pool了,還沒來得及刷入資料頁,那麼mysql突然當機,就會丢失資料。

此時redo log的作用就出來了,在寫入buffer pool後會同時寫入到redo log(順序寫磁盤)一份,redo log有固定的大小,會被重複使用。

MVCC及隔離級别

mvcc是什麼

MVCC (MultiVersion Concurrency Control) 叫做多版本并發控制。了解為:事務對資料庫的任何修改的送出都不會直接覆寫之前的資料,而是産生一個新的版本與老版本共存,使得讀取時可以完全不加鎖。

有點抽象是嗎,再來詳細解釋一下。同一行資料會有多個版本,某事務對該資料的修改并不會直接覆寫老版本,而是産生一個新版本和老版共存。然後在該行追加兩個虛拟的列,列就是進行資料操作的事務的ID(created_by_txn_id),是一個單調遞增的ID;還有一個deleted_by_txn_id,将來用來做删除的。

那麼在另一個事務在讀取該行資料時,由具體的隔離級别來控制到底讀取該行的哪個版本。同時,在讀取過程中完全不加鎖,除非用select * xxx for update強行加鎖。

譬如read committed級别,每次讀取,總是取事務ID最大的那個就好了。

對于Repeatable read,每次讀取時,總是取事務ID小于等于目前事務的ID的那些資料記錄。在這個範圍内,如果某一資料有多個版本,則取最新的。

MVCC在mysql中的實作依賴的是undo log與read view

undo log記錄某行資料的多個版本的資料;read view用來判斷目前版本資料的可見性。

mysql就是用MVCC來實作讀寫分離不加鎖的。

那麼MVCC裡多出來的那些版本的資料最終是要删除的,支援MVCC的資料庫套路一般差不多,都會有一個背景線程來定時清理那些肯定沒用的資料。隻要一個資料的deleted_by_txn_id不為空,并且比目前還沒結束的事務ID最小的一個還小,該資料就可以被清理掉了。在PostgreSQL中,該清理任務叫“vacuum”,在Innodb中,叫做“purge”。

隔離級别

隔離級别目的很明确,管理多個并發讀寫請求的通路順序,包括串行和并行,要在并發性能和讀取資料的正确性上做個權衡。

其中的兩個隔離級别Serializable和 Read Uncommited幾乎用不上,這裡不談。

Read Committed

能讀到其他事務已送出的内容,這是Springboot預設的隔離級别。一個事務在他送出之前的所有修改,對其他事務不可見。送出後,其他事務就能讀到了。在很多場景下這種邏輯是可以接受的。

在這個隔離級别下,讀取資料不加鎖而是使用MVCC機制,寫入資料就是排他鎖。該級别會産生不可重複讀和幻讀問題。

不可重複讀就是在一個事務内多次讀取的結果不一樣,這個很容易了解,上面講MVCC時也說了,該級别每次select時都會去讀取最新的版本,是以同一個事務内,也就是代碼前面一行select了,後面又select了,可能會select到不同的值。因為這兩次select過程中,有其他事務對select的行進行了事務送出,就會被select出來最新的。

幻讀,即一個事務能夠讀取到插入的新資料。會出現幻讀也是一樣的道理,第一次select時還沒值,再次select時又有值了。

mysql事務(包括redo log,undo log,MVCC)及事務實作原理

Repeatable Read

這個級别名字就是可重複讀,這是mysql預設的隔離級别。

為什麼能重複讀,前面講MVCC時也說了,這個級别下,一旦讀到某個版本,後續都是這個版本了,好比是一次快照,就不關心其他事務對該行資料的送出了,它隻認第一次讀取時的版本号。

這個級别在一些場景下很重要,如

     資料備份:

             例如資料庫S從資料庫M中複制資料,但是M又不停的在修改資料。S需要拿到M的一個資料快照,但又不能停M。

     資料合法性校驗:

             例如有兩張表,一張記錄了當時的交易總額,另一張記錄了每個交易的金額。那麼在讀取資料時,如果沒有快照的存在,交易總額就可能和當時的交易總額對不上。

該級别依然會出現幻讀的問題,repeatable是可以出現幻讀的,一個事務雖然不能讀取其他事務對現有資料的修改,但是能夠讀取到插入的新資料。

即便是MVCC也解決不了幻讀的問題,這裡有一篇講的原因。

mysql事務(包括redo log,undo log,MVCC)及事務實作原理

寫前提困境

盡管在MVCC的加持下Read Committed和Repeatable Read都可以得到很好的實作。但是對于某些業務代碼來講,在目前事務中看到/看不到其他的事務已經送出的修改的意義不是很大。這種業務代碼一般是這樣的:

    先讀取一段現有的資料

    在這個資料的基礎上做邏輯判斷或者計算;

    将計算的結果寫回資料庫。

這樣第三步的寫入就會依賴第一步的讀取。但是在1和3之間,不管業務代碼離得有多近,都無法避免其他事務的并發修改。換句話說,步驟1的資料正确是步驟3能夠在業務上正确的前提,這樣其實與MVCC都沒什麼關系了,因為我們想象中的要操作的資料和實際值并不一樣,無論怎麼步驟3的結果其實都不對了。

無論你用哪種隔離,你都無法解決第一步讀取的資料和第三步操作之間,别的事務對它的修改。

解決方法:

mysql事務(包括redo log,undo log,MVCC)及事務實作原理

結論

雖然上面寫了很多,也很複雜,貌似不上鎖怎麼都難以解決寫前提困境。而事實上,我們幾乎不用考慮這樣的場景,極少有可能說多個用戶端同時操作同一條資料,又剛好碰上需要抉擇read committed還是Repeatable read的困境。

結論很簡單,不管他就好,你幾乎沒機會碰到這樣的選擇困境。