MySQL事務(wù)保證數(shù)據(jù)一致性的核心講解
前言
在后端開發(fā)中,你是否遇到過這樣的糟心場(chǎng)景?用戶支付成功,余額扣了但訂單卻沒生成;批量導(dǎo)入數(shù)據(jù)時(shí)突然斷電,重啟后部分?jǐn)?shù)據(jù)重復(fù)、部分?jǐn)?shù)據(jù)丟失……這些“數(shù)據(jù)翻車”的背后,幾乎都藏著一個(gè)共同的漏洞——沒用好 MySQL 事務(wù)。
如果把數(shù)據(jù)庫比作一個(gè)繁忙的倉庫,那數(shù)據(jù)操作就是工人搬運(yùn)貨物的過程。要是搬運(yùn)到一半突然停電,貨物扔在半路、貨架空空如也,整個(gè)倉庫就會(huì)陷入混亂。而 MySQL 事務(wù),就是給這個(gè)搬運(yùn)過程裝了一道“安全鎖”:要么把所有貨物完好地搬到位,要么一旦出問題就退回原點(diǎn),絕不留下半拉子工程。今天我們就扒透事務(wù)的底層邏輯,教你用它把數(shù)據(jù)安全牢牢握在手里。
一、事務(wù)的概念以及使用場(chǎng)景
事務(wù)把一組SQL語句打包成為一個(gè)整體,在這組SQL的執(zhí)行過程中,要么全部成功,要么全部失敗。這組SQL語句可以是一條也可以是多條。
以一個(gè)轉(zhuǎn)賬例子為例:

初始狀態(tài):

先進(jìn)行張三余額減少100
UPDATE bank_account set balance = balance - 100 where name = '張三';

在進(jìn)行李四余額增加100
UPDATE bank_account set balance = balance + 100 where name = '李四';

如果轉(zhuǎn)賬成功,應(yīng)該有以下結(jié)果:
- 張三的賬戶余額減少 100 ,變成 900 ,李四的賬戶余額增加了 100 ,變成 1100 ,不能出現(xiàn)張三的余額減少而李四的余額沒有增加的情況;
- 張三和李四在發(fā)生轉(zhuǎn)賬前后的總額不變,也就是說轉(zhuǎn)賬前張三和李四的余額總數(shù)為1000+1000=2000 ,轉(zhuǎn)賬后他們的余額總數(shù)為 900+1100=2000 ;
- 轉(zhuǎn)賬后的余額結(jié)果應(yīng)當(dāng)保存到存儲(chǔ)介質(zhì)中,以便以后讀??;
- 還有一點(diǎn)需要要注意,在轉(zhuǎn)賬的處理過程中張三和李四的余額不能因其他的轉(zhuǎn)賬事件而受到干擾;
這四點(diǎn)在事務(wù)的整個(gè)執(zhí)行過程中必須要得到保證,這也就是事務(wù)的 ACID 特性
二、事務(wù)的ACID特性
數(shù)據(jù)安全的“四梁八柱”
事務(wù)的ACID特性指的是 Atomicity (原子性), Consistency (一致性), Isolation (隔離性) 和 Durability (持久性)。
1. Atomicity (原子性)
一個(gè)事務(wù)中的所有操作,要么全部成功,要么全部失敗,不會(huì)出現(xiàn)只執(zhí)行了一半的情況,如果事務(wù)在執(zhí)行過程中發(fā)生錯(cuò)誤,會(huì)回滾( Rollback )到事務(wù)開始前的狀態(tài),就像這個(gè)事務(wù)從來沒有執(zhí)行過一樣;
2. Consistency (一致性)
在事務(wù)開始之前和事務(wù)結(jié)束以后,數(shù)據(jù)庫的完整性不會(huì)被破壞。這表示寫入的數(shù)據(jù)必須完全符合所有的預(yù)設(shè)規(guī)則,包括數(shù)據(jù)的精度、關(guān)聯(lián)性以及關(guān)于事務(wù)執(zhí)行過程中服務(wù)器崩潰后如何恢復(fù);
3. Isolation (隔離性)
數(shù)據(jù)庫允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)數(shù)據(jù)進(jìn)行讀寫和修改,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)?時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致。事務(wù)可以指定不同的隔離級(jí)別,以權(quán)衡在不同的應(yīng)?場(chǎng)景下數(shù)據(jù)庫性能和安全;
4. Durability (持久性)
事務(wù)處理結(jié)束后,對(duì)數(shù)據(jù)的修改將永久的寫入存儲(chǔ)介質(zhì),即便系統(tǒng)故障也不會(huì)丟失
三、為什么要使用事務(wù)
事務(wù)具備的ACID特性,是我們使用事務(wù)的原因,在我們?nèi)粘5臉I(yè)務(wù)場(chǎng)景中有大量的需求要用事務(wù)來保證。支持事務(wù)的數(shù)據(jù)庫能夠簡(jiǎn)化我們的編程模型, 不需要我們?nèi)タ紤]各種各樣的潛在錯(cuò)誤和并發(fā)問題,在使用事務(wù)過程中,要么提交,要么回滾,不用去考慮網(wǎng)絡(luò)異常,服務(wù)器宕機(jī)等其他因素,因此我們經(jīng)常接觸的事務(wù)本質(zhì)上是數(shù)據(jù)庫對(duì) ACID 模型的一個(gè)實(shí)現(xiàn),是為應(yīng)用層服務(wù)的。
四、那么該如何使用事務(wù)呢
要使用事務(wù)那么數(shù)據(jù)庫就要支持事務(wù),在MySQL中支持事務(wù)的存儲(chǔ)引擎是InnoDB,可以通過show engines; 語句查看:

語法:
開始一個(gè)新的事務(wù)
START TRANSACTION; # 或 BEGIN;
提交當(dāng)前事務(wù),并對(duì)更改持久化保存
COMMIT;
回滾當(dāng)前事務(wù),取消其更改
ROLLBACK;
• START TRANSACTION 或 BEGIN 開始?個(gè)新的事務(wù);
• COMMIT 提交當(dāng)前事務(wù),并對(duì)更改持久化保存;
• ROLLBACK 回滾當(dāng)前事務(wù),取消其更改;
• 無論提交還是回滾,事務(wù)都會(huì)關(guān)閉
開啟一個(gè)事務(wù),執(zhí)行修改后回滾
開啟事務(wù)

表中數(shù)據(jù)

執(zhí)行sql使張三余額減少100

執(zhí)行sql使李四余額增加100

發(fā)現(xiàn)此時(shí)的余額相比之前已經(jīng)修改了
執(zhí)行回滾操作

發(fā)現(xiàn)回到了初始狀態(tài)值
開啟?個(gè)事務(wù),執(zhí)行修改后提交
開啟事務(wù),先查看表中的初始值

再次執(zhí)行張三減100,李四加100操作


進(jìn)行事務(wù)提交

再查詢發(fā)現(xiàn)數(shù)據(jù)已被修改,說明數(shù)據(jù)已經(jīng)持久化到磁盤

在事務(wù)執(zhí)行的過程中設(shè)置保存點(diǎn),回滾時(shí)指定保存點(diǎn)可以把數(shù)據(jù)恢復(fù)到保存點(diǎn)的狀態(tài)
在次進(jìn)行開啟事務(wù)并對(duì)張三減100,李四加100操作

我們來設(shè)置保存點(diǎn),看看最終效果如何
SAVEPOINT savepoint1;
對(duì)張三減100,李四加100操作

設(shè)置第?個(gè)保存點(diǎn)
SAVEPOINT savepoint2;
插入一條新記錄
insert into bank_account values (null, '王五', 1000);

回滾到第二個(gè)保存點(diǎn)
ROLLBACK TO savepoint2;
發(fā)現(xiàn)回滾到了插入王五數(shù)據(jù)前

回滾到第?個(gè)保存點(diǎn)

當(dāng)回滾時(shí)不指定保存點(diǎn),直接回滾到事務(wù)開始時(shí)的原始狀態(tài),事務(wù)關(guān)閉

自動(dòng)/手動(dòng)提交事務(wù)
查看當(dāng)前事務(wù)是否自動(dòng)提交可以使?以下語句
show variables like 'autocommit';

設(shè)置事務(wù)自動(dòng)提交
mysql> SET AUTOCOMMIT=1; 方式一
mysql> SET AUTOCOMMIT=ON; 方式二
設(shè)置事務(wù)手動(dòng)提交
mysql> SET AUTOCOMMIT=0; 方式一
mysql> SET AUTOCOMMIT=OFF;方式二
•只要使用START TRANSACTION 或 BEGIN 開啟事務(wù),必須要通過 COMMIT 提交才會(huì)持久化,與是否設(shè)置 SET autocommit 無關(guān)。
• 手動(dòng)提交模式下,不用顯示開啟事務(wù),執(zhí)行修改操作后,提交或回滾事務(wù)時(shí)直接使用 commit或 rollback
• 已提交的事務(wù)不能回滾
五、進(jìn)階
隔離級(jí)別——解決“并發(fā)事務(wù)”的沖突
MySQL服務(wù)可以同時(shí)被多個(gè)客戶端訪問,每個(gè)客戶端執(zhí)行的DML語句以事務(wù)為基本單位,那么不同的客戶端在對(duì)同一張表中的同一條數(shù)據(jù)進(jìn)行修改的時(shí)候就可能出現(xiàn)相互影響的情況,為了保證不同的事務(wù)之間在執(zhí)行的過程中不受影響,那么事務(wù)之間就需要要相互隔離,這種特性就是隔離性。
隔離性是為了解決“多個(gè)事務(wù)同時(shí)執(zhí)行”的沖突。但“隔離”的程度不同,性能和安全性也會(huì)有差異。MySQL 提供了 4 種隔離級(jí)別,從低到高分別是:
| 隔離級(jí)別 | 可能出現(xiàn)的問題 | 適用場(chǎng)景 |
|---|---|---|
| 讀未提交(Read Uncommitted) | 臟讀、不可重復(fù)讀、幻讀 | 幾乎不用(安全性太低) |
| 讀已提交(Read Committed) | 不可重復(fù)讀、幻讀 | 大多數(shù)互聯(lián)網(wǎng)場(chǎng)景(如電商訂單查詢) |
| 可重復(fù)讀(Repeatable Read) | 幻讀(MySQL 已通過間隙鎖解決) | 金融、支付等對(duì)數(shù)據(jù)一致性要求高的場(chǎng)景 |
| 串行化(Serializable) | 無(完全隔離) | 極少用(性能極低,適合單線程操作) |
事務(wù)的隔離級(jí)別分為全局作用域和會(huì)話作用域,查看不同作用域事務(wù)的隔離級(jí)別,可以使用以下的方式:
全局作用域
SELECT @@GLOBAL.transaction_isolation;
會(huì)話作用域
SELECT @@SESSION.transaction_isolation;
可以看到默認(rèn)的事務(wù)隔離級(jí)別是REPEATABLE-READ(可重復(fù)讀)

設(shè)置事務(wù)的隔離級(jí)別和訪問模式,可以使用以下語法:
通過GLOBAL|SESSION分別指定不同作用域的事務(wù)隔離級(jí)別
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode;
# 隔離級(jí)別
level: {
REPEATABLE READ # 可重復(fù)讀
| READ COMMITTED # 讀已提交
| READ UNCOMMITTED # 讀未提交
| SERIALIZABLE # 串?化
}
# 訪問模式
access_mode: {
READ WRITE # 表?事務(wù)可以對(duì)數(shù)據(jù)進(jìn)?讀寫
| READ ONLY
}
# ?式? SET GLOBAL transaction_isolation = 'SERIALIZABLE'; # 注意使?SET語法時(shí)有空格要?"-"代替 SET SESSION transaction_isolation = 'REPEATABLE-READ'; # ?式? SET @@GLOBAL.transaction_isolation='SERIALIZABLE'; # 注意使?SET語法時(shí)有空格要?"-"代替 SET @@SESSION.transaction_isolation='REPEATABLE-READ';
不同隔離級(jí)別存在的問題:
READ UNCOMMITTED - 讀未提交與臟讀
出現(xiàn)在事務(wù)的 READ UNCOMMITTED 隔離級(jí)別下,由于在讀取數(shù)據(jù)時(shí)不做任何限制,所以并發(fā)性能很高,但是會(huì)出現(xiàn)大量的數(shù)據(jù)安全問題,比如在事務(wù)A中執(zhí)行了?條 INSERT 語句,在沒有執(zhí)行COMMIT 的情況下,會(huì)在事務(wù)B中被讀取到,此時(shí)如果事務(wù)A執(zhí)行回滾操作,那么事務(wù)B中讀取到事務(wù)A寫入的數(shù)據(jù)將沒有意義,我們把這個(gè)理象叫做 “臟讀”.
臟讀:事務(wù)A修改了工資(但沒提交),事務(wù)B讀到了這個(gè)“未提交的修改”;之后事務(wù)A回滾,事務(wù)B讀到的就是“臟數(shù)據(jù)”(比如A把工資從5000改成8000,B看到8000,A回滾后,B以為工資還是8000)。
READ COMMITTED - 讀已提交與不可重復(fù)讀
為了解決臟讀問題,可以把事務(wù)的隔離級(jí)別設(shè)置為 READ COMMITTED ,這時(shí)事務(wù)只能讀到了其他事務(wù)提交之后的數(shù)據(jù),但會(huì)出現(xiàn)不可重復(fù)讀的問題,比如事務(wù)A先對(duì)某條數(shù)據(jù)進(jìn)?了查詢,之后事務(wù)B對(duì)這條數(shù)據(jù)進(jìn)行了修改,并且提交( COMMIT )事務(wù),事務(wù)A再對(duì)這條數(shù)據(jù)進(jìn)行查詢時(shí),得到了事務(wù)B修改之后的結(jié)果,這導(dǎo)致了事務(wù)A在同一個(gè)事務(wù)中以相同的條件查詢得到了不同的值,這個(gè)現(xiàn)象要"不可重復(fù)讀"。
不可重復(fù)讀:事務(wù)B第一次查工資是5000,事務(wù)A修改工資為8000并提交;事務(wù)B再次查工資,變成了8000——同一事務(wù)內(nèi),兩次讀的結(jié)果不一致。
REPEATABLE READ - 可重復(fù)讀與幻讀
為了解決不可重復(fù)讀問題,可以把事務(wù)的隔離級(jí)別設(shè)置為 REPEATABLE READ ,這時(shí)同一個(gè)事務(wù)中讀取的數(shù)據(jù)在任何時(shí)候都是相同的結(jié)果,但還會(huì)出現(xiàn)?個(gè)問題,事務(wù)A查詢了一個(gè)區(qū)間的記錄得到結(jié)果集A,事務(wù)B向這個(gè)區(qū)間的間隙中寫入了一條記錄并提交,事務(wù)A再查詢這個(gè)區(qū)間的結(jié)果集時(shí)會(huì)查到事務(wù)B新寫入的記錄得到結(jié)果集B,兩次查詢的結(jié)果集不一致,這個(gè)現(xiàn)象就是"幻讀"。
幻讀:事務(wù)B查詢“工資>5000的員工數(shù)”是10人,事務(wù)A新增了一個(gè)工資6000的員工并提交;事務(wù)B再次查詢,結(jié)果變成11人——像出現(xiàn)了“幻覺”。
注意:隔離級(jí)別越高,數(shù)據(jù)越安全,但并發(fā)性能越低。比如“串行化”會(huì)把事務(wù)變成“排隊(duì)執(zhí)行”,雖然不會(huì)有任何沖突,但在高并發(fā)場(chǎng)景(如秒殺)下,會(huì)直接導(dǎo)致系統(tǒng)卡死——所以千萬別盲目追求“最高隔離級(jí)別”。

六、避坑指南
這些事務(wù)“陷阱”一定要避開
即使理解了 ACID 和隔離級(jí)別,實(shí)際開發(fā)中還是容易踩坑。分享 3 個(gè)最常見的陷阱:
1. 陷阱1
把“非事務(wù)安全表”當(dāng)事務(wù)表用
MySQL 中,只有 InnoDB 引擎支持事務(wù),MyISAM、MEMORY 等引擎不支持事務(wù)。如果你的表是 MyISAM 引擎,就算寫了 BEGIN TRANSACTION 和 COMMIT,也不會(huì)有任何事務(wù)效果——執(zhí)行到一半失敗,數(shù)據(jù)照樣會(huì)“翻車”。
解決辦法:創(chuàng)建表時(shí)明確指定 InnoDB 引擎:
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
balance INT NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
2. 陷阱2
在事務(wù)中執(zhí)行“非事務(wù)操作”
比如在事務(wù)里執(zhí)行 DROP TABLE、ALTER TABLE 等 DDL 語句——這些語句會(huì)自動(dòng)提交事務(wù),導(dǎo)致之前的操作被強(qiáng)制 COMMIT,后續(xù)的 ROLLBACK 失效。
例子:
START TRANSACTION; UPDATE user SET balance = balance - 100 WHERE id = 1; -- 第一步 ALTER TABLE user ADD COLUMN phone VARCHAR(20); -- 執(zhí)行DDL,自動(dòng)提交事務(wù) UPDATE user SET balance = balance + 100 WHERE id = 2; -- 這步會(huì)開啟新事務(wù) ROLLBACK; -- 只能回滾第二步,第一步的“減100”已經(jīng)被提交,無法回滾
解決辦法:DDL 語句不要放在事務(wù)里執(zhí)行;如果必須執(zhí)行,先確保當(dāng)前沒有未提交的事務(wù)。
3. 陷阱3
事務(wù)過大,導(dǎo)致鎖等待或死鎖
如果一個(gè)事務(wù)包含大量操作(比如批量更新10萬條數(shù)據(jù)),會(huì)導(dǎo)致事務(wù)執(zhí)行時(shí)間過長(zhǎng),占用數(shù)據(jù)庫鎖的時(shí)間也變長(zhǎng)——其他事務(wù)需要等待這個(gè)鎖釋放,容易出現(xiàn)“鎖等待超時(shí)”;如果兩個(gè)事務(wù)互相等待對(duì)方的鎖,還會(huì)導(dǎo)致“死鎖”。
解決辦法:
- 拆分大事務(wù):把“批量更新10萬條”拆成100次“每次更新1000條”,每次執(zhí)行完一個(gè)小事務(wù)就提交;
- 避免長(zhǎng)事務(wù):事務(wù)中不要包含用戶輸入、網(wǎng)絡(luò)請(qǐng)求等耗時(shí)操作(比如在事務(wù)里調(diào)用第三方支付接口,接口卡30秒,事務(wù)就會(huì)鎖30秒)。 七:總結(jié):事務(wù)不是“銀彈”,但沒有事務(wù)萬萬不能
MySQL 事務(wù)不是“萬能藥”,它不能解決所有數(shù)據(jù)問題(比如硬件物理損壞需要靠備份恢復(fù)),但它是保障“數(shù)據(jù)一致性”的基礎(chǔ)——沒有事務(wù),任何涉及多步操作的數(shù)據(jù)場(chǎng)景,都可能出現(xiàn)“翻車”風(fēng)險(xiǎn)。
最后,用三句話總結(jié)事務(wù)的核心用法:
- 牢記 ACID:原子性保障“不丟步”,一致性保障“不違規(guī)”,隔離性保障“不沖突”,持久性保障“不丟失”;
- 選對(duì)隔離級(jí)別:默認(rèn)用“可重復(fù)讀”,互聯(lián)網(wǎng)場(chǎng)景可降為“讀已提交”,金融場(chǎng)景不升為“串行化”;
- 避開常見陷阱:用 InnoDB 引擎,不混放 DDL 語句,拆分大事務(wù)。
掌握了這些,你就能讓數(shù)據(jù)從“脆弱易翻車”變得“穩(wěn)如泰山”,再也不用為“錢扣了沒到賬”“庫存超賣”這類問題頭疼了!
以上就是MySQL事務(wù)保證數(shù)據(jù)一致性的核心講解的詳細(xì)內(nèi)容,更多關(guān)于MySQL數(shù)據(jù)一致性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何解決mysql的count()函數(shù)條件表達(dá)式不生效問題
該文章總結(jié)了SQL查詢中`count`函數(shù)統(tǒng)計(jì)錯(cuò)誤的原因,以及三種解決方法:使用`ornull`方法、`IF()`函數(shù)和`casewhen`表達(dá)式,當(dāng)不滿足條件時(shí),表達(dá)式的值為NULL2024-11-11
MYSQL必知必會(huì)讀書筆記第三章之顯示數(shù)據(jù)庫
MySQL是一種開放源代碼的關(guān)系型數(shù)據(jù)庫管理系統(tǒng)(RDBMS),MySQL數(shù)據(jù)庫系統(tǒng)使用最常用的數(shù)據(jù)庫管理語言--結(jié)構(gòu)化查詢語言(SQL)進(jìn)行數(shù)據(jù)庫管理。接下來通過本文給大家介紹MYSQL必知必會(huì)讀書筆記第三章之顯示數(shù)據(jù)庫,感興趣的朋友參考下吧2016-05-05
關(guān)于mysql中string和number的轉(zhuǎn)換問題
這篇文章主要介紹了關(guān)于mysql中string和number的轉(zhuǎn)換問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
mysql設(shè)置遠(yuǎn)程訪問數(shù)據(jù)庫的多種方法
最近有一同學(xué)問我MySQL無法遠(yuǎn)程訪問怎么呢,但能使用localhost來進(jìn)行方法,下面腳本之家來給各位介紹一下解決辦法,需要的朋友可以參考下2013-10-10
Java的Struts框架中append標(biāo)簽與generator標(biāo)簽的使用
這篇文章主要介紹了Java的Struts框架中append標(biāo)簽與generator標(biāo)簽的使用方法,Struts是Java的SSH三大web開發(fā)框架之一,需要的朋友可以參考下2015-12-12
MySQL時(shí)間戳與日期格式的相互轉(zhuǎn)換
在MySQL數(shù)據(jù)庫中,時(shí)間戳和日期格式是常用的數(shù)據(jù)類型,在MySQL中,我們可以使用函數(shù)還相互轉(zhuǎn)換時(shí)間戳和日期格式,下面我將詳細(xì)的給大家介紹如何進(jìn)行轉(zhuǎn)換,并提供相應(yīng)的代碼示例,感興趣的小伙伴跟著小編一起來看看吧2024-01-01

