MySQL的事務(wù)機(jī)制與并發(fā)讀異常示例詳解
一、事務(wù)
事務(wù)是用戶定義的一系列操作,這些操作需要讓MySQL視為一個(gè)整體執(zhí)行,這些操作要么都做,要么都不做,是一個(gè)不可分割的工作單位。
1.1 概念
前提:有并發(fā)連接訪問,多條連接,并發(fā)執(zhí)行 sql 語句
事務(wù)是用戶定義的不可分割的操作序列,是并發(fā)控制的最小單元,這些操作要么全部執(zhí)行(提交),要么全部不執(zhí)行(回滾)。
目的:事務(wù)將數(shù)據(jù)庫從一種一致性狀態(tài)轉(zhuǎn)換為另一種一致性狀態(tài);保證系統(tǒng)的數(shù)據(jù)完整性與可靠性。
舉個(gè)例子:在銀行轉(zhuǎn)賬中,“A 扣款 + B 收款”就是一個(gè)典型事務(wù),要么兩個(gè)操作都成功,要么都失敗,不能只執(zhí)行一半。
組成:
- 簡(jiǎn)單事務(wù):可由單條 SQL 語句組成(如
UPDATE students SET age=26 WHERE id=15); - 復(fù)雜事務(wù):可由一組關(guān)聯(lián)的 SQL 語句組成(如轉(zhuǎn)賬場(chǎng)景中的 “扣款 + 收款” 兩條更新語句)。
特征:
- 在數(shù)據(jù)庫提交事務(wù)時(shí),可以確保要么所有修改都已經(jīng)保存,要么所有修改都不保存;
- 事務(wù)是訪問并更新數(shù)據(jù)庫各種數(shù)據(jù)項(xiàng)的一個(gè)程序執(zhí)行單元。
- 在 MySQL innodb 下,單條語句都具備事務(wù);可以通過
set autocommit = 0;設(shè)置當(dāng)前會(huì)話手動(dòng)提交;
1.2 事務(wù)控制語句
begin/start transactioncommitrollback
-- 顯示開啟事務(wù) START TRANSACTION | BEGIN -- 提交事務(wù),并使得已對(duì)數(shù)據(jù)庫做的所有修改持久化 COMMIT -- 回滾事務(wù),結(jié)束用戶的事務(wù),并撤銷正在進(jìn)行的所有未提交的修改 ROLLBACK -- 創(chuàng)建一個(gè)保存點(diǎn),一個(gè)事務(wù)可以有多個(gè)保存點(diǎn) SAVEPOINT identifier -- 刪除一個(gè)保存點(diǎn) RELEASE SAVEPOINT identifier -- 事務(wù)回滾到保存點(diǎn) ROLLBACK TO [SAVEPOINT] identifier
1.3 ACID特性
① 原子性(A)
事務(wù)是最小執(zhí)行單元,事務(wù)操作要么都做(提交),要么都不做(回滾);MySQL 通過 undo log 實(shí)現(xiàn)回滾,它記錄了每個(gè)操作的反向操作,從而在異常時(shí)恢復(fù)數(shù)據(jù)。
② 一致性(C)
事務(wù)執(zhí)行前后,數(shù)據(jù)庫必須處于一致狀態(tài)。
- 數(shù)據(jù)庫完整性約束(唯一、非空等)
- 邏輯上的一致性:用戶定義的一系列操作,需要數(shù)據(jù)庫視為一個(gè)整體,這是用戶定義的
事務(wù)執(zhí)行前后,數(shù)據(jù)庫的 “完整性約束”(如唯一鍵、非空約束)與 “邏輯一致性”(如轉(zhuǎn)賬總金額不變)不能被破壞。一致性由原子性、隔離性、持久性共同保障。
③ 隔離性(I)
描述:控制 “多個(gè)并發(fā)事務(wù)的相互影響程度”;各自運(yùn)行的環(huán)境隔離。
多個(gè)事務(wù)并發(fā)執(zhí)行時(shí),應(yīng)互不干擾。
一個(gè)事務(wù)內(nèi)部的操作和數(shù)據(jù),在其提交前對(duì)其他事務(wù)是不可見的。
InnoDB 用 MVCC(多版本并發(fā)控制) 處理 “讀 - 寫并發(fā)”(讀操作通過版本鏈讀歷史數(shù)據(jù),無需加鎖);用鎖(表鎖、頁鎖、行鎖等) 處理 “寫 - 寫并發(fā)”(寫操作加鎖避免沖突)。
MVCC 是多版本并發(fā)控制,主要解決一致性非鎖定讀,通過記錄和獲取行版本,而不是使用鎖來限制讀操作,從而實(shí)現(xiàn)高效并發(fā)讀性能。鎖用來處理并發(fā) DML 操作;數(shù)據(jù)庫中提供粒度鎖的策略,針對(duì)表(聚集索引 B+ 樹)、頁(聚集索引 B+ 樹葉子節(jié)點(diǎn))、行(葉子節(jié)點(diǎn)當(dāng)中某一段記錄行)三種粒度加鎖;
不同隔離級(jí)別會(huì)導(dǎo)致不同并發(fā)現(xiàn)象(臟讀、不可重復(fù)讀、幻讀)
④ 持久性(D)
事務(wù)一旦提交,數(shù)據(jù)的修改就會(huì)永久保存。
無論系統(tǒng)宕機(jī)、斷電還是重啟,數(shù)據(jù)都能通過 redo log(重做日志) 恢復(fù)。
redo log 記錄的是物理層面的“頁修改”信息,比如修改了哪個(gè)數(shù)據(jù)頁、偏移量、內(nèi)容等,保證事務(wù)的最終落盤。
1.4 隔離級(jí)別
ISO 和 ANIS SQL 標(biāo)準(zhǔn)制定了四種事務(wù)隔離級(jí)別的標(biāo)準(zhǔn),各數(shù)據(jù)庫廠商在正確性和性能之間做了妥協(xié),并沒有嚴(yán)格遵循這些標(biāo)準(zhǔn);MySQL innodb默認(rèn)支持的隔離級(jí)別是 REPEATABLE READ;
數(shù)據(jù)庫標(biāo)準(zhǔn)(ISO/ANSI SQL)定義了四種隔離級(jí)別,從低到高依次是:
| 隔離級(jí)別 | 說明 | 讀操作 | 寫操作 | 并發(fā)現(xiàn)象 | 性能特點(diǎn) |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 讀未提交,讀不加鎖,性能最高但風(fēng)險(xiǎn)最大 | 不加鎖,直接讀最新數(shù)據(jù) | 自動(dòng)加 X 鎖 | 可能臟讀、不可重復(fù)讀、幻讀 | 性能最高,安全性最差 |
| READ COMMITTED | 讀已提交;讀取最新已提交版本(支持 MVCC) | 通過 MVCC 讀已提交數(shù)據(jù) | 自動(dòng)加 X 鎖 | 避免臟讀,仍可能不可重復(fù)讀、幻讀 | 性能較好 |
| REPEATABLE READ(默認(rèn)) | 可重復(fù)讀,事務(wù)期間讀到的是事務(wù)開始時(shí)的數(shù)據(jù)快照(支持 MVCC) | 通過 MVCC 讀事務(wù)開始前版本 | 自動(dòng)加 X 鎖 | 避免臟讀、不可重復(fù)讀,但仍可能幻讀;MySQL 已基本避免幻讀 | 性能適中 |
| SERIALIZABLE | 可串行化;所有事務(wù)順序執(zhí)行 | 加 S 鎖(Next-Key Lock) | 加 X 鎖 | 完全避免并發(fā)異常 | 性能最差,最安全 |
1.5 命令
-- 設(shè)置隔離級(jí)別 SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 或者采用下面的方式設(shè)置隔離級(jí)別 SET @@tx_isolation = 'REPEATABLE READ'; SET @@global.tx_isolation = 'REPEATABLE READ'; -- 查看全局隔離級(jí)別 SELECT @@global.tx_isolation; -- 查看當(dāng)前會(huì)話隔離級(jí)別 SELECT @@session.tx_isolation; SELECT @@tx_isolation; -- 手動(dòng)給讀加 S 鎖 SELECT ... LOCK IN SHARE MODE; -- 手動(dòng)給讀加 X 鎖 SELECT ... FOR UPDATE; -- 查看當(dāng)前鎖信息 SELECT * FROM information_schema.innodb_locks;
二、并發(fā)讀異常
2.1 臟讀
事務(wù)(A)可以讀到另外一個(gè)事務(wù)(B)中未提交的數(shù)據(jù);也就是事務(wù)A讀到臟數(shù)據(jù);在讀寫分離的場(chǎng)景下,可以將 slave 節(jié)點(diǎn)設(shè)置為 READ UNCOMMITTED;此時(shí)臟讀不影響,在 slave 上查詢并不需要特別精準(zhǔn)的返回值。
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='READ UNCOMMITTED'; | SET @@tx_isolation='READ UNCOMMITTED'; |
| 2 | BEGIN; | |
| 3 | UPDATE account_t SET money = money - 100 WHERE name = 'A'; | |
| 4 | BEGIN; | |
| 5 | SELECT money FROM account_t WHERE name = 'A'; | |
| 6 | SELECT money FROM account_t WHERE name = 'B'; | |
| 7 | UPDATE account_t SET money = money - 100 WHERE name = 'B'; | |
| 8 | COMMIT | COMMIT |

2.2 不可重復(fù)讀
事務(wù)(A) 可以讀到另外一個(gè)事務(wù)(B)中提交的數(shù)據(jù);通常發(fā)生在一個(gè)事務(wù)中兩次讀到的數(shù)據(jù)是不一樣的情況;不可重復(fù)讀在隔離級(jí)別 READ COMMITTED 存在。一般而言,不可重復(fù)讀的問題是可以接受的,因?yàn)樽x到已經(jīng)提交的數(shù)據(jù),一般不會(huì)帶來很大的問題,所以很多廠商(如Oracle、SQL Server)默認(rèn)隔離級(jí)別就是 READ COMMITTED;
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='READ COMMITTED'; | SET @@tx_isolation='READ COMMITTED'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT money FROM account_t WHERE name = 'A'; | |
| 4 | UPDATE account_t SET money = money - 100 WHERE name = 'A'; | |
| 5 | COMMIT; | SELECT money FROM account_t WHERE name = 'A'; |
| 6 | COMMIT; |

2.3 幻讀
兩次讀取同一個(gè)范圍內(nèi)的記錄得到的結(jié)果集不一樣;快照讀和當(dāng)前讀不一致;
例如:以 name 為唯一鍵的表,一個(gè)事務(wù)中查詢 select * from t where name = 'gulu'; 不存在,接下來 insert into t(name) values ('gulu'); 出現(xiàn)錯(cuò)誤,因?yàn)榇藭r(shí)另外一個(gè)事務(wù)也執(zhí)行了 insert 操作;
幻讀在隔離級(jí)別 REPEATABLE READ 及以下存在;但是可以在 REPEATABLE READ 級(jí)別下通過讀加鎖(使用 next-key locking)解決;
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='REPEATABLE READ'; | SET @@tx_isolation='REPEATABLE READ'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT * FROM account_t WHERE id >= 2; | |
| 4 | INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); | |
| 5 | COMMIT; | |
| 6 | INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); | |
| 7 | COMMIT; |

解決:給讀操作加鎖
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='REPEATABLE READ'; | SET @@tx_isolation='REPEATABLE READ'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT * FROM account_t WHERE id >= 2 lock in share mode; | |
| 4 | INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); | |
| 5 | COMMIT; | |
| 6 | SELECT * FROM account_t WHERE id >= 2; | |
| 7 | COMMIT; |


2.4 丟失更新
臟讀、不可重復(fù)讀、幻讀都是一個(gè)事務(wù)寫,一個(gè)事務(wù)讀,由于一個(gè)事務(wù)的寫導(dǎo)致另一個(gè)事務(wù)讀到了不該讀的數(shù)據(jù);丟失更新是兩個(gè)事務(wù)都是寫;丟失更新分為提交覆蓋和回滾覆蓋;回滾覆蓋數(shù)據(jù)庫拒絕不可能產(chǎn)生,重點(diǎn)關(guān)注提交覆蓋;
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='REPEATABLE READ'; | SET @@tx_isolation='REPEATABLE READ'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT money FROM account_t WHERE name = 'A'; | |
| 4 | SELECT money FROM account_t WHERE name = 'A'; | |
| 5 | UPDATE account_t SET money = 1000+100 WHERE name = 'A'; | |
| 6 | COMMIT; | |
| 7 | UPDATE account_t SET money = 1000-100 WHERE name = 'A'; | |
| 8 | COMMIT; |

2.5 隔離級(jí)別下并發(fā)讀異常
| 隔離級(jí)別 | 回滾覆蓋 | 臟讀 | 不可重復(fù)讀 | 幻讀 | 提交覆蓋 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | no | yes | yes | yes | yes |
| READ COMMITTED | no | no | yes | yes | yes |
| REPEATABLE READ | no | no | no | yes(手動(dòng)加鎖) | yes(手動(dòng)加鎖) |
| SERIALIZABLE | no | no | no | no | no |
2.6 區(qū)別
- 臟讀和不可重復(fù)讀的區(qū)別在于,臟讀是讀取了另一個(gè)事務(wù)未提交的數(shù)據(jù),而不可重復(fù)讀是讀取了另一個(gè)事務(wù)提交之后的修改;本質(zhì)上都是其他事務(wù)的修改影響了本事務(wù)的讀??;
- 不可重復(fù)讀和幻讀比較類似;不可重復(fù)讀是兩次讀取同一條記錄,得到不一樣的結(jié)果;而幻讀是兩次讀取同一個(gè)范圍內(nèi)的記錄得到的結(jié)果集不一樣(可能不同個(gè)數(shù),也可能相同個(gè)數(shù)內(nèi)容不一樣,比如x一行后又添加新行);不可重復(fù)讀是因?yàn)槠渌聞?wù)進(jìn)行了 update 操作,幻讀是因?yàn)槠渌聞?wù)進(jìn)行了 insert 或者 delete 操作。
三、Redo Log(重做日志)
在 MySQL InnoDB 存儲(chǔ)引擎中,Redo Log 是實(shí)現(xiàn)事務(wù)持久性的核心機(jī)制。
簡(jiǎn)單來說,它用來保證事務(wù)提交后,即使系統(tǒng)宕機(jī),也能通過日志恢復(fù)到正確狀態(tài)。
在事務(wù)執(zhí)行過程中,數(shù)據(jù)的修改首先發(fā)生在內(nèi)存中的 Buffer Pool,但內(nèi)存數(shù)據(jù)存在易失性,一旦斷電或崩潰就會(huì)丟失。為了防止這種情況,InnoDB 設(shè)計(jì)了 Redo Log 來記錄數(shù)據(jù)的物理修改。
Redo Log 由兩部分組成:
- redo log buffer(內(nèi)存中):暫存事務(wù)執(zhí)行時(shí)產(chǎn)生的日志;
- redo log file(磁盤文件):持久化存儲(chǔ)日志記錄。
在事務(wù)提交(COMMIT)的過程中,必須先將 redo log buffer 寫入磁盤文件(刷盤),確認(rèn)日志已經(jīng)持久化后,事務(wù)才算真正提交成功。這就是事務(wù)的 WAL(Write-Ahead Logging,預(yù)寫日志)機(jī)制 —— 先寫日志,后寫數(shù)據(jù)。
Redo Log 的特性與作用
- 記錄內(nèi)容: 頁號(hào)、頁偏移量、修改內(nèi)容(物理變化);
- 寫入方式: 順序?qū)懭?,性能高?/li>
- 使用場(chǎng)景: 正常運(yùn)行時(shí)只寫不讀,只有在 宕機(jī)恢復(fù) 時(shí)才會(huì)被讀取,用于將數(shù)據(jù)恢復(fù)到最近一次提交的狀態(tài)。
- 作用總結(jié): 確保了數(shù)據(jù)的持久性(D)
四、Undo Log(回滾日志)
與 Redo Log 相對(duì),Undo Log 用來實(shí)現(xiàn)事務(wù)的原子性(Atomicity)和支持 MVCC(多版本并發(fā)控制)。
如果 Redo Log 是“向前重做”,Undo Log 就是“向后回滾”。
當(dāng)事務(wù)執(zhí)行時(shí),InnoDB 會(huì)為每一次數(shù)據(jù)修改生成相應(yīng)的 Undo 記錄,用來保存修改前的數(shù)據(jù)版本。這些 Undo 日志存儲(chǔ)在 共享表空間(Undo Tablespace) 中。
在事務(wù)回滾(ROLLBACK)時(shí),系統(tǒng)會(huì)根據(jù) Undo Log 執(zhí)行反向操作,把數(shù)據(jù)邏輯地恢復(fù)到修改前的狀態(tài):
- 若事務(wù)執(zhí)行了
INSERT,回滾時(shí)會(huì)執(zhí)行DELETE; - 若事務(wù)執(zhí)行了
DELETE,回滾時(shí)會(huì)執(zhí)行INSERT; - 若事務(wù)執(zhí)行了
UPDATE,回滾時(shí)會(huì)執(zhí)行相反方向的UPDATE。
Undo Log 的特性與作用
保證事務(wù)原子性:
如果事務(wù)執(zhí)行中途失敗,可以根據(jù) Undo Log 將已執(zhí)行的部分操作撤銷,確保“要么全部成功,要么全部失敗”。支撐 MVCC(多版本并發(fā)控制):
Undo Log 記錄了每行數(shù)據(jù)的歷史版本,通過它 MySQL 可以在不同事務(wù)中讀取到不同時(shí)刻的數(shù)據(jù)快照,從而實(shí)現(xiàn)非鎖定讀。記錄形式: Undo Log 是邏輯日志(記錄操作邏輯),不同于 Redo Log 的物理頁修改。
總結(jié)
到此這篇關(guān)于MySQL事務(wù)機(jī)制與并發(fā)讀異常的文章就介紹到這了,更多相關(guān)MySQL事務(wù)與并發(fā)讀異常內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mysql8.0.11數(shù)據(jù)目錄遷移的實(shí)現(xiàn)
這篇文章主要介紹了mysql8.0.11數(shù)據(jù)目錄遷移的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
navicat連接mysql報(bào)錯(cuò)1251的解決方法
這篇文章主要為大家詳細(xì)介紹了navicat連接mysql報(bào)錯(cuò)1251的解決方法,文中解決方法介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
MySQL 觸發(fā)器定義與用法簡(jiǎn)單實(shí)例
這篇文章主要介紹了MySQL 觸發(fā)器定義與用法,結(jié)合簡(jiǎn)單實(shí)例形式總結(jié)分析了mysql觸發(fā)器的語法、原理、定義及使用方法,需要的朋友可以參考下2019-09-09
老鳥帶你開發(fā)專業(yè)規(guī)范的MySQL啟動(dòng)腳本
這篇文章主要介紹了老鳥帶你開發(fā)專業(yè)規(guī)范的MySQL啟動(dòng)腳本,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
MySQL8.0.28安裝教程詳細(xì)圖解(windows?64位)
如果電腦上已經(jīng)有MySQL數(shù)據(jù)庫再進(jìn)行重做往往會(huì)遇到問題,下面這篇文章主要給大家介紹了關(guān)于windows?64位系統(tǒng)下MySQL8.0.28安裝教程的詳細(xì)教程,文章通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04

