文章目錄
- 事務
- ACID
- 并發帶來的隔離問題
- 幻讀(虛讀)
- 不可重復讀
- 臟讀
- 丟失更新
- 隔離級別
- Read Uncommitted (讀未提交)
- Read Committed (讀已提交)
- Repeatable Read (可重復讀)
- Serializable (可串行化)
- 事務的使用
- 事務的實現
- Redo
- undo
事務
事務指邏輯上的一組操作。
我們在 MySQL 存儲引擎 | MyISAM 與 InnoDB 中提到,MyISAM引擎
并不支持事務,所以本文內容主要與 InnoDB引擎
相關。
這篇博客寫事務也寫得很好:我以為我對Mysql事務很熟,直到我遇到了阿里面試官
ACID
談到事務,那肯定少不了ACID的特性,ACID是以下幾個單詞的縮寫:
原子性(atomicity):
事務是一個不可分割的工作單位,對數據的修改要么全部執行成功,要么全部失敗。
舉個例子:事務A中要進行轉賬,那么轉出的賬號要扣錢,轉入的賬號要加錢,這兩個操作都必須同時執行成功,從而確保數據的一致性。
一致性(consistency):
事務操作前與操作后數據庫的狀態始終一致。
如何理解呢?就好比我們此時有用戶A和用戶B,他們的余額分別為300元和700元,此時兩人總金額為1000元。此時若是用戶B向用戶A轉賬200元,則兩者的此時都有500元,總金額還是1000元。
也就是說,無論我們兩個怎么轉賬,總金額它只會是1000,既不會多,也不會少。這就是事務操作前后的狀態始終一致。倘若錢多了或者少了,都代表著事務將數據庫從一種狀態變為了另外一種狀態,此時就不再符合一致性了。
隔離性(isolation):
隔離性指的是每個讀寫事務的對象之間相互隔離,即該事務提交前對其他事務都不可見。
持久性(durability):
持久性指的是事務一旦提交,這個事務的狀態會被持久化到數據庫中。 即使發生了服務器宕機的事故,數據庫也能成功的將數據給恢復。
但是需要注意的是,只能保證數據庫本身發生的問題后可以恢復,但并不是事務提交后所有變化都是永久的,倘若是由于外部原因如:RAID卡損壞、天災人禍導致數據庫發生問題,那么即使事務提交了,也可能會丟失。
基于上述原因,持久性只能保證事務系統的高可靠性,而無法保證其高可用性。
總結:
原子性、隔離性、持久性都是為了保障一致性而存在的,一致性也是最終的目的。
并發帶來的隔離問題
幻讀(虛讀)
幻讀指 在同一事務中,用同樣的操作讀取兩次,得到的記錄數卻不一樣(針對 insert 操作)。 舉個例子:
- 第一個事務對表中的所有數據行進行修改;
- 同時,第二個事務向表中插入了一行。這樣也就導致了操作第一個事務的用戶發現表中還有沒修改的數據行,像發生了幻覺一樣。
明明在 會話A
的第一次查詢中,大于 2
的數只有行只有一行,而由于 會話B
插入了新行后,對于 會話A
而言就憑空多出來了一行,像出現了幻覺一樣。
不可重復讀
不可重復讀指的是在一個事務中多次讀取同一行數據,但是多次讀取的數據卻不一樣(針對 update 操作)。導致這一問題的主要原因就是一個事務讀取到了其他事務已提交的數據。
例如:
- 賬戶1中有300元,賬戶2中有500元
- 事務A讀取賬戶B的內容,里面顯示有500元
- 事務B將賬戶1的300元全部轉給賬戶2,并提交事務
- 事務A再次讀取賬戶B,此時里面有800元。
由于其他事務的干擾,對于事務A來說,兩次讀取的金額都不一樣。
因為不可重復讀讀到的是已經提交的數據,由于其本身并不會帶來很大的問題,所以大部分數據庫廠商都會允許這種情況的發生。
臟讀
臟讀即一個事務讀取到了另外一個事務中未提交的數據,也就是可能因為其他事務對數據進行修改或者回滾導致的問題。
會話B
在第一次查看時表中只有一條數據,但是在第五階段中 會話A
向表中插入了另一條數據(但還未commit【提交】
),這就導致了 會話B
在讀取的時候得到的結果就不再一樣,因為它讀取到了臟數據。
臟讀的現象并不會經常發生,因為臟讀發生的條件是需要事務的隔離級別為 READ UNCOMMITTED(讀未提交)
,而大部分數據庫的默認隔離級別都為 READ COMMITTED(讀已提交)
。
丟失更新
丟失更新就是一個事務的更新操作會被另外一個事務的更新操作所覆蓋,從而導致數據的不一致。例如以下案例:
事務A
將行記錄r
更新為1
,但是事務A
并未提交;- 同時,
事務B
將行記錄r
更新為2
,事務B
也未提交; - 事務A提交;
- 事務B提交。
此時由于 B
將 A
的修改覆蓋,導致 A
雖然提交,但是更新卻丟失了,只剩下了 B
的更新。
但是在當前數據庫的任何隔離級別下,都不會導致理論意義上的丟失更新問題,即使是隔離級別最低的 Read Uncommitted
,也由于加鎖保護,所以 事務B
的修改操作會被阻塞,直到 事務A
提交。
隔離級別
為了解決上述問題,MySQL中實現了以下四種隔離級別,隔離級別由低到高依次是:
- 讀未提交(READ UNCOMMITTED)
- 讀已提交 (READ COMMITTED)
- 可重復讀 (REPEATABLE READ)
- 串行化 (SERIALIZABLE)
隔離級別越高,事務請求的鎖也就越多,保持鎖的時間也就越長。所以隔離性越強,并發的效率也就越低。
Read Uncommitted (讀未提交)
在該隔離級別下,所有事務都可以看到其他未提交事務的執行結果,容易產生臟讀問題。
在該級別下,雖然并發的效率最高,但是安全性完全沒有得到保護,所以很少用于實際應用。
Read Committed (讀已提交)
該隔離級別是大部分數據庫默認的隔離級別,如 Oracle
、SQL Server
等。該隔離級別下,一個事務只能看見提交了的事務所做的改變,容易產生不可重復讀的問題。
雖然它還有,但不可重復讀本身并不是一個大問題,所以為了兼顧到性能,大部分數據庫都會容許這種問題的產生。
Repeatable Read (可重復讀)
這是 MySQL
中 InnoDB
默認的隔離級別,它確保同一事務的多個實例在并發讀取數據時,會看到同樣的數據行,容易產生幻讀的問題。
InnoDB
可以借助 MVCC
中的 Next-Key Locking
的加鎖方式來解決這個問題,詳見本文。
Serializable (可串行化)
這是最高的隔離級別,通過強制事務進行排序,使事務之間不可能互相沖突,從而解決了其他隔離級別無法解決的幻讀問題。
由于其在每個讀的數據行上加了共享鎖,所以在該隔離級別下可能會導致大量的超時現象以及鎖競爭。
這四種隔離級別分別可能發生的問題如下圖所示:
事務的使用
開啟事務
START TRANSACTION
或者
BEGIN
(由于MySQL的數據分析器會自動將BEGIN識別為BEGIN...END,所以在存儲過程中只能使用START TRANSACTION來開啟事務)
提交事務
COMMIT
回滾事務
//回滾整個事務
ROLLBACK//回滾至某個保存點
ROLLBACK TO SAVEPOINT [保存點ID]
設置保存點
SAVEPOINT 保存點ID
刪除保存點
RELEASE SAVEPOINT 保存點ID
查看隔離級別
// 可以看到 MySQL 的 InnoDB存儲引擎 的默認隔離級別為可重復讀
mysql> SELECT @@TRANSACTION_ISOLATION;
+-------------------------+
| @@TRANSACTION_ISOLATION |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
設置隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
// GLOBAL 也可以換成 SESSION,前者表示全局的,后者表示當前會話,也就是當前窗口有效。
PS:當設置完隔離級別后對于之前打開的會話,是無效的,要重新打開一個窗口設置隔離級別才生效。
事務的實現
- 事務的持久性主要依靠 Redo log(重做日志)來完成。
- 原子性、一致性則通過 Undo log(撤銷日志)來完成。
- 隔離性,則通過鎖來完成。
先簡單說說 Redo 和 Undo :
Redo/Undo機制: 將所有對數據的更新操作都寫到日志中。
Redo log
用來記錄某數據塊被修改后的值,可以用來恢復未寫入data file
的已成功事務更新的數據。Redo
通常是物理日志,記錄的是頁的物理修改操作。Undo log
是用來記錄數據更新前的值,保證數據更新失敗能夠回滾。Undo
是邏輯日志,根據每行記錄進行記錄。
舉個例子:
假如某個時刻數據庫崩潰,當數據庫重啟進行 crash-recovery
時,就會通過 Redo log
將已經提交事務的更改寫到數據文件,而還沒有提交的就通過 Undo log
進行 回滾roll back
。
Redo
Redo log
由兩部分組成:
- 一是存在于內存中的 重做日志緩沖(redo log buff),由于存在內存中,所以其具有易失性。
- 二是 重做日志文件(redo log file),其存在于硬盤中,所以是持久的。
Redo
主要通過 Force Log at Commit機制
來實現事務的持久性。
步驟如下:
為了確保日志寫入文件中,每次將日志緩沖寫入日志文件后,都會發起一次 異步操作(fsync) 。
為什么需要這個異步調用呢?
因為重做日志文件打開時并沒有使用 O_DIRECT 選項
,所以重做日志緩沖會先寫入文件系統緩沖,為了保證其能夠成功寫入磁盤,必須發起一次異步調用。由于異步調用的效率取決于磁盤的性能,因此磁盤的性能決定了事務提交的性能,即數據庫性能。
undo
undo
是撤銷日志,其中保留了數據庫各個版本的狀態,我們可以借助 undo
邏輯地將數據庫恢復到原來地樣子。除了進行回滾之外, undo
的另一個作用就是實現 MVCC
。
首先看看 undo log
的生成流程:
每當事務發生變更的時候,都會伴隨著 undo log
的產生,并且為了防止其丟失,undo log 會比數據先持久化到硬盤上。
由于 undo log
是邏輯日志,所以其中記錄的都是對于數據庫的操作指令。而事務的回滾,其實也就是根據這個操作來進行一個逆向操作。如下面幾種:
- 當執行一個
insert
指令時,其逆向指令為delete
; - 當執行一個
delete
指令時,其逆向指令為insert
; - 當執行一個
update
指令時,其逆向指令為update
。
原子性就是借助以上機制實現,倘若事務中的某一個步驟未能成功完成,則借助 undo log
中存儲的記錄來回滾到事務的最原始狀態,即一個失敗全體失敗。
而至于一致性,則主要依靠上述的其他三種特性來實現,也就是說一致性是目的,而原子性、隔離性、持久性則是數據庫實現一致性的手段,只有滿足這三個性質,才能夠保證一致性。