前言:本篇文章基於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(原子性、持久性和隔離性)來保證數據在事務執行前後的一致性。
- 舉個例子:
A向B轉賬,A和B錢的金額被約束不能小於0
假設轉賬之前,A有500元,B有500元,A和B的總和是1000元
那麼A向B轉賬500之後,A有0元,B有1000元,A和B的錢加起來還是1000元
這個就是客觀上的事務的一致性,即程序寫的邏輯是正確的,從認知上來看也是正常的,同時滿足約束條件,事務會執行成功,事務執行前A和B的總和是1000元,事務執行後A和B的總和還是1000元,所以該事務滿足一致性。
- 再舉個例子:
A向B轉賬,A和B錢的金額被約束不能小於0
假設轉賬之前,A有500元,B有500元,A和B的總和是1000元
A要向B轉賬1000元,如果轉賬成功後A會有-500元,B有1500元,A和B的錢加起來還是1000元
雖然A和B的錢總和還是1000元,但是不滿足約束,此時會回滾
該場景中,事務執行前A和B的總和是1000元,事務執行後A和B的總和還是1000元,雖然滿足客觀上的一致性,但是不滿足約束條件,就把數據還原到了事務執行前的狀態來保證一致性,所以也可以說約束條件爲事務提供了一致性的保證。
- 再舉個例子:
我去銀行存錢,賬戶存之前是0元
我要存1000元,賬戶存完是1000元
從數據上看,賬戶金額是不一致的,從客觀的角度來看,雖然你手裏沒有了1000元,但是你賬戶上也多了1000元,這是的的確確存在的,所以同樣滿足一致性的。
什麼是主觀一致性?
開發者故意寫出違反約束的代碼,導致事務能正常執行完成,從數據的角度來看,執行前後的狀態是一致的,但是從認知的角度來看確實不一致的。
舉個例子:
A向B轉賬,A和B錢的金額沒有約束限制
假設轉賬之前,A有500元,B有500元,A和B的總和是1000元
A要向B轉賬1000元,轉賬後A有-500元,B有1500元,A和B的錢加起來還是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 來解決的,該鎖保證了在執行當前事務時,其他事務不能對當前事務作用的區間進行新增。 - 快照讀場景:
- 什麼是快照讀?
快照讀只是簡單的select操作(不包括 select … for update,select … lock in share mode)
在同一個事務中,事務開始時,第一條select查詢會將結果集生成一個快照(snapshot),然後,還是在這個事務中,第二次查詢同一條數據時,會直接查詢第一次查詢時生成的快照,這個查詢過程就叫做快照讀,這樣就避免了幻讀問題。 - 快照讀怎麼實現的?
快照讀是由MVCC實現的,MVCC是多版本併發控制,快照就是其中的一個版本。 - 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節點查詢出的結果是一樣的,沒有出現幻讀。
- 當前讀場景:
-
什麼是當前讀?
在事務中進行 insert/delete/update/select … for update/select … lock in share mode 操作時,這個場景就爲當前讀。 -
修改爲何會產生幻讀?
在一個事務中,在進行修改操作時會觸發行級鎖,行鎖只能鎖住行,也就是隻能鎖住已存在的數據,但是新插入的這個記錄是不存在的,所以無法鎖住,如果這時另一個事務做了一個插入操作,而本事務用到了另一個事務插入的數據,就會產生幻讀。 -
當前讀場景如何解決幻讀?
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加載到內存裏邊,那內存就能恢復到掛掉之前的數據了,這樣就保證了持久性。