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