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

之前寫了幾篇mysql存儲原理的文章。

6 Innodb_buffer_pool

b+ tree和每個page存儲結構

4 innodb文件系統基本結構(段、簇、頁面)

innodb文件系統初步入門

表對象緩存

1 連接層

這一篇終於到事務了,事務大家都知道ACID概念,那麼mysql是如何完成事務的呢?

先來看結論——

原子性:

一次事務中的所有操作,要麼全部完成要麼全部不執行。這裏是通過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元到理財賬戶。

可以看到,每一條變更數據的操作,都伴隨一條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 logread 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時又有值了。

Repeatable Read

這個級別名字就是可重複讀,這是mysql默認的隔離級別。

爲什麼能重複讀,前面講MVCC時也說了,這個級別下,一旦讀到某個版本,後續都是這個版本了,好比是一次快照,就不關心其他事務對該行數據的提交了,它只認第一次讀取時的版本號。

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

     數據備份:
             例如數據庫S從數據庫M中複製數據,但是M又不停的在修改數據。S需要拿到M的一個數據快照,但又不能停M。
     數據合法性校驗:
             例如有兩張表,一張記錄了當時的交易總額,另一張記錄了每個交易的金額。那麼在讀取數據時,如果沒有快照的存在,交易總額就可能和當時的交易總額對不上。

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

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

寫前提困境

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

    先讀取一段現有的數據

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

    將計算的結果寫回數據庫。

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

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

解決方法:

結論

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章