大師,我想悟透MySQL數據庫的事務!

前言:本篇文章基於MySQL的InnoDB引擎。

什麼是事務?

事務是一個邏輯概念,在這個邏輯概念中,由多個不同的動作組成,在執行多個動作時,人爲的把這多個動作當做一個業務邏輯來處理,對於一個業務邏輯來說,結果只有成功或失敗,當多個動作都成功時,這個業務邏輯纔會被認爲是成功,而事務的目的就是要保證這些動作要麼都成功,要麼都失敗。

四個特性ACID

ACID特性:指的是原子性(atomicity),一致性(consistency),隔離性(isolation),持久性(durability)。

  • 原子性(Atomicity):事務作爲一個整體被執行,包含在其中的對數據庫的操作要麼全部被執行,要麼都不執行。
  • 一致性(Consistency):事務應確保數據庫的狀態從一個一致狀態轉變爲另一個一致狀態。一致狀態應滿足完整性約束。
  • 隔離性(Isolation):事務的隔離性是多個用戶併發訪問數據庫時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作數據所幹擾,多個併發事務之間要相互隔離。
  • 持久性(Durability):一個事務一旦提交,他對數據庫的修改應該永久保存在數據庫中。

如何保證原子性

MySQL事務的原子性是通過undo log來保證的。

那什麼是undo log?

在數據進行修改(即DML和DDL操作)的時候,會記錄undo log,即和binlog相反的日誌記錄,存儲的主要是邏輯日誌。

  • 舉個例子,當新增一條數據時,會在binlog中記錄一條insert日誌,同時會在undo log記錄的一條與binlog相反的delete日誌。
    同樣,在update一條記錄時,binlog會記錄update日誌,undo log會記錄一條與binlog相反的update記錄。

undo log有什麼作用?

undo log 主要用來做回滾和多版本併發控制(MVCC)。

  • 回滾:當執行 rollback 時,就可以從undo log中的讀取到相應的內容並進行回滾。
  • 版本控制:用到行版本控制的時候,也是通過undo log來實現,當讀取的某一行被其他事務鎖定時,它可以從undo log中分析出該行記錄以前的數據是什麼,從而提供該行版本信息,讓用戶實現非鎖定一致性讀取。

引出結論,如何保證原子性?

  • 當事務錯誤時,即事務中的一部分操作已經成功,但另一部分操作失敗,這時會執行回滾,使用undo log產生的相反的操作,這樣就能將已經執行成功的操作撤銷,從而達到回滾的目的。因爲支持回滾操作,所以我們就能保證“要麼全部被執行,要麼都不執行”。

  • 當事務成功時,當事務提交的時候,innodb不會立即刪除undo log,因爲後續還可能會用到undo log,如隔離級別爲repeatable read時,事務讀取的都是開啓事務時的最新提交行版本,只要該事務不結束,該行版本就不能刪除,從而保證其他事務發生異常時能正常回滾。

如何保證一致性

一致性這個概念比較抽象,我先來說下我對一致性的理解:一致性分爲主觀一致性和客觀一致性,同時還需要滿足約束條件。

什麼是客觀一致性?

客觀一致性的意思就是程序在滿足約束條件的情況下,通過AID(原子性、持久性和隔離性)來保證數據在事務執行前後的一致性。

  • 舉個例子:
AB轉賬,AB錢的金額被約束不能小於0
假設轉賬之前,A500元,B500元,AB的總和是1000元
那麼AB轉賬500之後,A0元,B1000元,AB的錢加起來還是1000

這個就是客觀上的事務的一致性,即程序寫的邏輯是正確的,從認知上來看也是正常的,同時滿足約束條件,事務會執行成功,事務執行前A和B的總和是1000元,事務執行後A和B的總和還是1000元,所以該事務滿足一致性。

  • 再舉個例子:
AB轉賬,AB錢的金額被約束不能小於0
假設轉賬之前,A500元,B500元,AB的總和是1000A要向B轉賬1000元,如果轉賬成功後A會有-500元,B1500元,AB的錢加起來還是1000元
雖然AB的錢總和還是1000元,但是不滿足約束,此時會回滾

該場景中,事務執行前A和B的總和是1000元,事務執行後A和B的總和還是1000元,雖然滿足客觀上的一致性,但是不滿足約束條件,就把數據還原到了事務執行前的狀態來保證一致性,所以也可以說約束條件爲事務提供了一致性的保證。

  • 再舉個例子:
我去銀行存錢,賬戶存之前是0元
我要存1000元,賬戶存完是1000

從數據上看,賬戶金額是不一致的,從客觀的角度來看,雖然你手裏沒有了1000元,但是你賬戶上也多了1000元,這是的的確確存在的,所以同樣滿足一致性的。

什麼是主觀一致性?

開發者故意寫出違反約束的代碼,導致事務能正常執行完成,從數據的角度來看,執行前後的狀態是一致的,但是從認知的角度來看確實不一致的。
舉個例子:

AB轉賬,AB錢的金額沒有約束限制
假設轉賬之前,A500元,B500元,AB的總和是1000A要向B轉賬1000元,轉賬後A-500元,B1500元,AB的錢加起來還是1000

事務執行前A和B的總和是1000元,事務執行後A和B的總和還是1000元,從數據上看雖然是一致的,但是從主觀認知上看,A賬戶變成負值了,這明顯不合法,所以不滿足主觀上的一致性;所以開發者要滿足一致性,需主觀去控制正確的代碼邏輯。

引出結論,如何保證一致性?

事實上,原子性、持久性和隔離性都是爲了保證一致性,並且需要主觀去控制正確的約束,來保證主觀和客觀上都一致。

如何保證隔離性

MySQL給出了四種隔離級別來保證隔離性。

四種隔離級別:Serializable、repeatable read、read committed、Read uncommitted

Serializable(串行化):事務之間以一種串行的方式執行,可避免髒讀、不可重複讀、幻讀(虛讀)情況的發生,安全性最高,效率也最低。
  • 髒讀:指在一個事務內讀取到了另外一個事務未提交的更新數據
  • 不可重複讀:指在一個事務內讀取到了另一個事務已提交的更新數據
  • 幻讀:指在一個事務內讀取到了另一個事務的已提交的新增數據
Repeatable Read(可重複讀):是MySQL默認的隔離級別,同一個事務中相同的查詢會看到同樣的數據行,可避免髒讀、不可重複讀情況的發生,安全性較高,效率較高。
  • 按理說,Repeatable Read隔離級別應該都會出現幻讀,但是在快照讀(snapshot read)場景下並不會出現幻讀,這是因爲MySQL在該隔離級別下用了 MVCC(多版本併發控制) 解決了該問題;
    當然,還是有幻讀的情況存在的,在當前讀(current read)場景下,涉及到數據的修改時有可能出現幻讀,因爲數據修改時,MySQL必須拿到最新的數據才能修改,不過當前讀可以通過 Next-Key Lock 來解決的,該鎖保證了在執行當前事務時,其他事務不能對當前事務作用的區間進行新增。
  • 快照讀場景:
  1. 什麼是快照讀?
    快照讀只是簡單的select操作(不包括 select … for update,select … lock in share mode)
    在同一個事務中,事務開始時,第一條select查詢會將結果集生成一個快照(snapshot),然後,還是在這個事務中,第二次查詢同一條數據時,會直接查詢第一次查詢時生成的快照,這個查詢過程就叫做快照讀,這樣就避免了幻讀問題。
  2. 快照讀怎麼實現的?
    快照讀是由MVCC實現的,MVCC是多版本併發控制,快照就是其中的一個版本。
  3. MVCC怎麼實現的?
    而MVCC是由undo log實現的,因爲undo log存儲着每次修改的數據,並且會記錄B_ROLL_PTR(回滾指針,上一個版本的數據在undo log中的位置),同時會記錄DB_TRX_ID(事務ID),所以可以通過DB_ROLL_PTR可以找到各個歷史版本,並且由DB_TRX_ID決定使用哪個版本的快照。
  • 舉個例子
時間 事務A 事務B
time1 set autocommit=0 set autocommit=0
time2 start transaction start transaction
time3 select count(*) from tab
time4 insert into tab(id,age) values(10,18)
time5 commit
time6 select count(*) from tab

實測結果:time3節點和time6節點查詢出的結果是一樣的,沒有出現幻讀。

  • 當前讀場景:
  1. 什麼是當前讀?
    在事務中進行 insert/delete/update/select … for update/select … lock in share mode 操作時,這個場景就爲當前讀。

  2. 修改爲何會產生幻讀?
    在一個事務中,在進行修改操作時會觸發行級鎖,行鎖只能鎖住行,也就是隻能鎖住已存在的數據,但是新插入的這個記錄是不存在的,所以無法鎖住,如果這時另一個事務做了一個插入操作,而本事務用到了另一個事務插入的數據,就會產生幻讀。

  3. 當前讀場景如何解決幻讀?
    Innodb在Repeatable Read隔離級別下,通過next-key lock機制避免了幻讀現象,next-key loc,實現相當於record lock(行級鎖) + gap lock(間隙鎖),該鎖不僅會鎖住記錄本身,還會鎖定一個範圍,gap lock(間隙鎖)是指對於鍵值在條件範圍內但並不存在的記錄加鎖,這樣其他事務就無法對間隙鎖內的數據進行修改,從而解決幻讀。
    間隙鎖默認是關閉的,可通過innodb_locks_unsafe_for_binlog = 1是開啓。

  • 舉個會產生幻讀的例子
時間 事務A 事務B
time1 set autocommit=0 set autocommit=0
time2 start transaction start transaction
time3 select * from tab
time4 insert into tab(id,age) values(11,18)
time5 commit
time6 update tab set age = 28 where id =11
time7 select * from tab

實測結果:time3 查詢時未普通查詢,不會加間隙鎖,time3節點和time7節點查詢出的結果是不一樣的,time7查出了id爲11、age爲28的數據,出現了幻讀。

  • 舉個不會產生幻讀的例子
時間 事務A 事務B
time1 set autocommit=0 set autocommit=0
time2 start transaction start transaction
time3 select * from tab where id > 10 lock in share mode
time4 insert into tab(id,age) values(11,18) (此時會阻塞,等待A事務結束,或等待超時)
time5 update tab set age = 28 where id =11
time6 select * from tab where id > 10

實測結果:time3 查詢時加了lock in share mode(也可以用for update),這時會對id>10的這一範圍內存在的間隙加鎖,所以time3節點和time6節點查詢出的結果是一樣的,沒有出現幻讀,而此時的事務B會被阻塞,等待A事務結束或超時

Read Commited(讀已提交):一個事務可以讀到另一個事務已經提交的數據,安全性較低,效率較高
Read Uncommited(讀未提交):一個事務可以讀到另一個事務未提交的數據,安全性低,效率高

如何保證持久性

持久性是由redo log保證的。

什麼是redo log?

當數據庫進行增刪改時,會記錄的是物理修改的內容(xxxx頁修改了xxx)到redo log中。

redo log有什麼作用?

redo log在事務開始的時候,就開始記錄每次修改的信息,InnoDB引擎會先將記錄寫到redo log中,再寫到內存中,最後再寫到磁盤,這份redo log記載着這次在某個頁上做了什麼修改;

引出結論,那redo log如何保證持久性的?

MySQL引入了redo log,當數據庫進行增刪改時記錄redo log日誌,所以,當我們修改的時候,寫完內存了,但數據還沒真正寫到磁盤,此時我們的數據庫掛了,我們可以根據redo log來對數據進行恢復,將redo log加載到內存裏邊,那內存就能恢復到掛掉之前的數據了,這樣就保證了持久性。

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