Mysql事物的持久性及原子性詳解
前言
本篇文章是分享學(xué)習(xí)《鳳凰架構(gòu)》這本書的筆記。感興趣的朋友,京東商城有售,也有電子版,強(qiáng)烈推薦大家去了解一下。
記得剛學(xué)習(xí)數(shù)據(jù)庫時(shí),老師講到事務(wù)就提到了 ACID 這個字眼。那時(shí)候比較懵逼,考試時(shí)記不住這4個單詞所有的含義,反正寫個 ACID 的話,2分也能拿一分。一直到工作了好幾年之后,準(zhǔn)確來說到我學(xué)習(xí)這個之前,其實(shí)一直持續(xù)懵逼的狀態(tài)就是:你問事務(wù),我就說 ACID,主打一個不深究放過自己。
就像當(dāng)年面試人家問什么是 MVC,我就說就是那個3個圈圈,模型視圖控制器,具體不太好描述。不過目前為止真正的行家遇到比較少,幾乎都能糊弄過去。我猜大概率可能是人家不好揭穿我,也有可能是跟我一樣在不求甚解的階段。至于哪一種也不重要,目前來說有AI學(xué)東西還是很快的,雖然無法決定我的上限,但它能顯著提升我能力的下限。
下面復(fù)習(xí)一遍數(shù)據(jù)庫事務(wù)。數(shù)據(jù)庫管理系統(tǒng)在寫入或者更新數(shù)據(jù)的過程中,為了保證數(shù)據(jù)是正確可靠的,需要滿足四個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability),簡稱 ACID。這次學(xué)習(xí)的步驟是先學(xué)習(xí)原子性和持久性,至于隔離性后續(xù)再分享。
?? ACID 核心特性
| 特性 | 英文 | 核心定義 | 關(guān)鍵說明 |
|---|---|---|---|
| 原子性 | Atomicity | 不可分割 | 事務(wù)中的操作要么全部完成,要么全部不完成。若發(fā)生錯誤,系統(tǒng)會回滾(Rollback)到事務(wù)開始前的狀態(tài)。 |
| 一致性 | Consistency | 數(shù)據(jù)完整性 | 事務(wù)執(zhí)行前后,數(shù)據(jù)庫的完整性約束沒有被破壞。寫入的數(shù)據(jù)必須完全符合所有的預(yù)設(shè)規(guī)則。 |
| 隔離性 | Isolation | 并發(fā)互不干擾 | 多個并發(fā)事務(wù)之間相互隔離,防止交叉執(zhí)行導(dǎo)致數(shù)據(jù)不一致。(如讀未提交、可重復(fù)讀等) |
| 持久性 | Durability | 永久生效 | 事務(wù)一旦提交,對數(shù)據(jù)的修改就是永久的。即使后續(xù)發(fā)生系統(tǒng)故障或斷電,數(shù)據(jù)也不會丟失。 |
1.為什么說 ACID 的提法不太嚴(yán)謹(jǐn)?
在學(xué)習(xí)《鳳凰架構(gòu)》后,原文中總結(jié)的一點(diǎn)讓我豁然開朗:
“對這四種特性是不太嚴(yán)謹(jǐn)?shù)?,因?yàn)檫@四種特性并不正交。A、I、D 是手段,C 是目的,為了拼湊個單詞縮寫才弄到一塊去。其實(shí)誤導(dǎo)的弊端已經(jīng)超過了易于傳播的好處。”
當(dāng)一個服務(wù)只操作一個數(shù)據(jù)源的時(shí)候,通過 A、I、D 來獲得一致性是相對容易的。但當(dāng)一個服務(wù)涉及到多個不同的數(shù)據(jù)源,甚至多個不同服務(wù)同時(shí)涉及到多個不同的數(shù)據(jù)源時(shí),就變得非常困難。
舉個經(jīng)典的購物例子,成功售出后,需要確保以下三步被正確地處理:
1.減去用戶賬號中的錢,修改用戶余額。
UPDATE user_money SET balance = balance - 100 WHERE user_id = 123 AND balance >= 100;
2.修改庫存的數(shù)量,然后把商品狀態(tài)標(biāo)為待配送。
UPDATE goods_info SET stock = stock - 1, status = '待配送' WHERE id = 4;
3.商家的賬號增加錢。
UPDATE user_wallet SET balance = balance + 100 WHERE user_id = 2;
這里我們要區(qū)分兩種場景:
場景一:
本地事務(wù)。如果這三個表恰好都在同一個數(shù)據(jù)庫實(shí)例里,那事情就很簡單,使用本地事務(wù)包裹這三條 SQL 即可。
場景二:
分布式事務(wù)。如果是微服務(wù)架構(gòu),用戶庫、商品庫和商家錢包庫通常是物理隔離的。此時(shí)簡單的本地事務(wù)就無能為力了,需要引入 TCC、Saga 等方案。
這一次我們主要聚焦在第一種場景——本地事務(wù),看看數(shù)據(jù)庫是如何在底層實(shí)現(xiàn)它的。
2.數(shù)據(jù)庫是如何實(shí)現(xiàn)原子性和持久性的?
原子性和持久性在事務(wù)里是密切相關(guān)的兩個屬性:
- 原子性保證了事務(wù)的多個操作要么都生效,要么都不生效,不會存在中間狀態(tài)。
- 持久性保證了一旦事務(wù)生效,就不會再因?yàn)槿魏卧蚨鴮?dǎo)致其修改的內(nèi)容被撤銷或丟失。也就是說,數(shù)據(jù)必須要成功寫入磁盤后才能擁有持久性。
但是實(shí)現(xiàn)它們也有困難,因?yàn)?strong>寫入磁盤這個操作不是原子的,不僅有寫入與未寫入,還存在著“正在寫”的中間狀態(tài)。接下來我們結(jié)合代碼代入使用場景進(jìn)一步體會:
using (var scope = new TransactionScope())
{
using (var conn = new SqlConnection(ConnectionString))
{
conn.Open();
string sql1 = "UPDATE user_money SET balance = balance - 100 WHERE user_id = 123 AND balance >= 100"; // 1. 減去用戶賬號中的錢
ExecuteNonQuery(conn, sql1);
string sql2 = "UPDATE goods_info SET stock = stock - 1, status = '待配送' WHERE id = 4"; // 2. 修改庫存數(shù)量,并把商品狀態(tài)標(biāo)為待配送
ExecuteNonQuery(conn, sql2);
string sql3 = "UPDATE user_wallet SET balance = balance + 100 WHERE user_id = 2"; // 3. 商家的賬號增加錢
ExecuteNonQuery(conn, sql3);
}
scope.Complete(); //關(guān)鍵分水嶺
}?? 崩潰場景推演
- 未提交事務(wù)崩潰:如果程序還沒走到
scope.Complete(),數(shù)據(jù)庫已經(jīng)將其中一個數(shù)據(jù)的變動寫入了磁盤,此時(shí)服務(wù)器崩潰。重啟之后,數(shù)據(jù)庫必須想辦法得知崩潰前發(fā)生過一次不完整的操作,將已經(jīng)修改過的數(shù)據(jù)恢復(fù)成原來的樣子,以保證原子性。 - 已提交事務(wù)崩潰:如果已經(jīng)執(zhí)行了
scope.Complete(),程序日志記錄了成功,但數(shù)據(jù)庫還未將全部三個數(shù)據(jù)的變動都寫入到磁盤,此時(shí)服務(wù)器崩潰。重啟之后,數(shù)據(jù)庫也必須要有辦法得知崩潰前發(fā)生過一次完整的操作,將還沒來得及寫入磁盤的那部分?jǐn)?shù)據(jù)重新寫入,以保證持久性。
為了解決這些問題,數(shù)據(jù)庫界探索出了不同的策略。
?? 策略一:Commit Logging(追加日志)
這種思路比較簡單:先把我要做的事情記錄到磁盤中(比如修改什么數(shù)據(jù)、從什么值改成什么值),寫完了日志用一個特殊狀態(tài)來標(biāo)記下,然后再修改數(shù)據(jù)。
流程大致如下:
修改xxxx放到第15頁 (Update Log) 修改xxx放到第39頁 (Update Log) Commit Record 開始在后臺真實(shí)改第15頁的那個數(shù)據(jù) 開始在后臺改第39頁的那個數(shù)據(jù) 改完了寫一條 End record
當(dāng) Complete 命令執(zhí)行時(shí),數(shù)據(jù)庫首先將 Commit Record 寫入日志文件并強(qiáng)制刷盤。此時(shí)真實(shí)數(shù)據(jù)文件尚未被修改,僅日志中記錄了事務(wù)已準(zhǔn)備好提交的狀態(tài)。
- 保證持久性:如果在修改真實(shí)數(shù)據(jù)時(shí)崩潰,重啟后根據(jù)已經(jīng)寫入磁盤的日志信息恢復(fù)現(xiàn)場、繼續(xù)修改數(shù)據(jù)即可。
- 保證原子性:如果
End record日志沒有寫入成功就發(fā)生崩潰,系統(tǒng)重啟后會看到一部分沒有Commit Record的日志,那將這部分日志標(biāo)記為回滾狀態(tài)即可。

?? 策略二:WAL 機(jī)制(Write-Ahead Logging)
Commit Logging 存在一個巨大的缺陷:性能瓶頸。所有對數(shù)據(jù)的真實(shí)修改都必須發(fā)生在事務(wù)提交、日志寫入了 Commit Record 之后。假設(shè)一個大型事務(wù)執(zhí)行了 10 秒鐘,這 10 秒內(nèi)產(chǎn)生的大量數(shù)據(jù)修改只能堆積在內(nèi)存里。直到第 10 秒末提交時(shí),數(shù)據(jù)庫才必須手忙腳亂地把積攢的數(shù)據(jù)一股腦地往磁盤上寫。這就導(dǎo)致了平時(shí)磁盤閑得發(fā)慌,提交時(shí)磁盤忙到卡死。有沒有辦法解決呢?我們先看兩個專業(yè)定義:
FORCE:當(dāng)事務(wù)提交后,要求變動數(shù)據(jù)必須同時(shí)完成寫入。
NO-FORCE:不強(qiáng)制變動數(shù)據(jù)必須同時(shí)完成寫入。
STEAL:在事務(wù)提交前,允許變動數(shù)據(jù)提前寫入。
NO-STEAL:不允許在事務(wù)提交前提前寫入。
大白話理解就是:立馬寫磁盤叫 FORCE,反之 NO-FORCE;偷偷寫點(diǎn)叫 STEAL,不準(zhǔn)偷偷寫叫 NO-STEAL。
Commit Logging 允許 NO-FORCE,但不允許 STEAL。因?yàn)榧偃缡聞?wù)提交前就有部分變動數(shù)據(jù)寫入磁盤,那一旦事務(wù)要回滾,這些提前寫入的變動數(shù)據(jù)就都成了錯誤。
現(xiàn)代數(shù)據(jù)庫(如 MySQL InnoDB)采用了 WAL 機(jī)制,它既允許 NO-FORCE,也允許 STEAL。為了實(shí)現(xiàn)這一點(diǎn),它引入了兩種關(guān)鍵日志:
- 1.Undo Log(回滾日志):為了支持 STEAL。在數(shù)據(jù)提前寫入磁盤前,先記錄修改前的舊值。一旦事務(wù)回滾或崩潰,就利用它來擦除那些提前寫入的“臟數(shù)據(jù)”,保證原子性。
- 2.Redo Log(重做日志):為了支持 NO-FORCE。在數(shù)據(jù)被修改前,先強(qiáng)制將物理修改記錄刷入磁盤。一旦提交后發(fā)生崩潰,就利用它來重放那些還沒來得及寫入磁盤的數(shù)據(jù),保證持久性。

組合角度分析:
- STEAL + NO-FORCE:允許偷偷寫(需 Undo Log 當(dāng)后悔藥),允許提交后不實(shí)時(shí)寫(需 Redo Log 當(dāng)記事本)。這是現(xiàn)代數(shù)據(jù)庫的主流選擇。
- NO-FORCE + NO-STEAL:不允許偷寫,允許提交后不實(shí)時(shí)寫,所以只需要 Redo Log。
- FORCE + NO-STEAL:沒偷寫,而且強(qiáng)制寫磁盤,所以不需要日志(性能最差)。
3.總結(jié)
我們可以得出以下結(jié)論:
1. 為什么需要 NO-FORCE 與 Redo Log?
絕大多數(shù)數(shù)據(jù)庫都采用 NO-FORCE 策略來優(yōu)化 I/O 性能。為了實(shí)現(xiàn)它,就需要引入 Redo Log。即使修改數(shù)據(jù)時(shí)系統(tǒng)崩潰了,重啟后數(shù)據(jù)庫可以根據(jù) Redo Log 恢復(fù)現(xiàn)場。
通俗理解:我先把要改的東西記錄在日志里,再根據(jù)日志統(tǒng)一寫到磁盤中。萬一我在寫入磁盤的過程中“暈倒”(宕機(jī))了,等我“醒來”(重啟)的時(shí)候,照著日志重新做一遍,也能成功。
2. 為什么需要 STEAL 與 Undo Log?
為了進(jìn)一步提升性能,允許在事務(wù)提交前“偷偷地”先寫一點(diǎn)數(shù)據(jù)到磁盤(STEAL)。但這帶來了新問題:萬一事務(wù)回滾,這些提前寫入的數(shù)據(jù)就變成了“臟數(shù)據(jù)”。這就需要引入 Undo Log,在偷摸寫入數(shù)據(jù)之前記錄舊值,以便隨時(shí)恢復(fù)。
3. Binlog 與 SQL Server 的事務(wù)日志
最后額外說一下,在 MySQL 中還有一個 Binlog 的概念。它和 Redo Log 所處的層面不一樣:
- Redo Log:處于 InnoDB 存儲引擎層,主要負(fù)責(zé)底層的崩潰恢復(fù)。
- Binlog:處于 MySQL Server 層,主要負(fù)責(zé)主從復(fù)制和數(shù)據(jù)恢復(fù)。
為了保證 Binlog 與 InnoDB 引擎層的 Redo Log 之間的數(shù)據(jù)一致,MySQL 引入了 2PC(兩階段提交) 機(jī)制。而在 SQL Server 中,也有差不多的概念,叫做事務(wù)日志(Transaction Log),它的功能更廣,同時(shí)承擔(dān)了類似 Redo Log 和 Undo Log 的職責(zé)。
到此這篇關(guān)于Mysql事物的持久性及原子性的文章就介紹到這了,更多相關(guān)mysql持久性和原子性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Myeclipse連接mysql數(shù)據(jù)庫心得體會
這篇文章主要為大家詳細(xì)介紹了MyEclipse連接MySQL數(shù)據(jù)庫圖文教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
mysql一對多關(guān)聯(lián)查詢分頁錯誤問題的解決方法
這篇文章主要介紹了mysql一對多關(guān)聯(lián)查詢分頁錯誤問題的解決方法,非常不錯,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09
MySQL?時(shí)區(qū)與?serverTimezone詳解
存儲?TIMESTAMP?類型數(shù)據(jù)時(shí),MySQL 會根據(jù)當(dāng)前會話的時(shí)區(qū)將時(shí)間轉(zhuǎn)換為 UTC 時(shí)間,MySQL 實(shí)際存儲的是 UTC 時(shí)間,這篇文章主要介紹了MySQL?時(shí)區(qū)與?serverTimezone,需要的朋友可以參考下2024-12-12
MySQL中列轉(zhuǎn)行和行轉(zhuǎn)列總結(jié)解決思路
最近工作中用到了好幾次列轉(zhuǎn)行,索性做個小總結(jié),下面這篇文章主要給大家介紹了關(guān)于MYSQL如何列轉(zhuǎn)行的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01
MySQL批量處理圖片URL統(tǒng)一去掉域名前綴的方法
文章介紹了如何使用MySQL 8.0的新函數(shù)REGEXP_REPLACE()批量處理圖片URL,去掉域名前綴,將所有圖片路徑統(tǒng)一存為相對路徑,從而避免路徑重復(fù)和加載錯誤,需要的朋友可以參考下2025-11-11

