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

初始狀態(tài):

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

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

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

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

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

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

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

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

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

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


進行事務提交

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

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

我們來設置保存點,看看最終效果如何
SAVEPOINT savepoint1;
對張三減100,李四加100操作

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

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

回滾到第?個保存點

當回滾時不指定保存點,直接回滾到事務開始時的原始狀態(tài),事務關閉

自動/手動提交事務
查看當前事務是否自動提交可以使?以下語句
show variables like 'autocommit';

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

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

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

