文章目錄
我對事務會比較懵逼。啥原子性,啥一致性,啥不可重複讀,啥幻讀…等等。而且自從
Spring
生態越來越好之後,使用事務僅僅需要一個註解就可以了,開發過程中對數據庫的事務的感知也越來越弱了。今天我就重新去理解數據庫事務,爭取寫完這篇文章就能記住爲啥會出現這些詞語。
什麼是事務?
事務就是一組原子性的 SQL 查詢,或者說一個獨立的工作執行單元。如果數據庫引擎能夠成功地對數據庫應用該組查詢的全部語句,那麼執行該組查詢。如果其中的一條語句因爲崩潰互毆其他原因無法執行,那麼所有的語句都不會執行。總的來說,事務內的語句,要麼全部執行成功,要麼全部執行失敗。
數據庫事務的 ACID
數據庫事務的 ACID
是一種準則。也就是說,一個合格的事務應該具備 ACID
這幾個特性。ACID
由以下幾個選項組成:
- Atomicity 原子性,指一個事務必須視爲一個不可分割的最小單元
- Consistency 一致性,
- Isolation 隔離性,通常來說,一個事務所做的修改在最終提交以前,對其他事物是不可見的
- Durability 持久型,一旦事務提交了,修改將永遠的存儲在數據庫當中。
我們看見上面幾個特性,雖然看起來挺簡單的。但是實際上,數據庫對此作出了大量大量工作,確保了 ACID
這個特性。
直接這麼說可能你感受不了太多,讓我分別舉例子。
假設你開發了一個銀行的應用。這個應用的數據庫有一張儲蓄表。假設儲蓄表存儲了小明和小丁的賬戶餘額信息。
原子性
有一天,小明想要轉1000塊給小丁。那麼作爲程序猿的你,需要在代碼層面有幾個操作
開啓事務
數據庫執行扣費 sql,小明的賬號扣除1000,小明的賬號扣除1000
然後給小丁的賬戶加1000
提交事務
假設如果當前數據庫引擎事務不支持原子性,當執行到第3步的時候,數據庫突然宕機了,第三步執行失敗。那麼就發生這種情況的後果是:小明的賬戶少了1000,但是小丁的賬戶卻沒有多1000,錢不翼而飛。這種涉及錢的事情,當然不能忍。所以,如果數據庫引擎是支持原子性的話,即使因爲宕機或者別的原因導致事務執行失敗,事務會直接回滾,錢會回到小明的賬戶,小丁的賬戶也不會多錢。
一致性
數據庫總是從一個一致性的狀態到另外一個一致性的狀態。
或許你會說,這個怎麼和原子性這麼像?
其實和原子性是有區別的。原子性側重點在於能否將將整個事務完成並提交,要麼一起成功要麼一起失敗;而一致性的側重點在於事務執行前的執行目標結果是否能和執行後的結果一致。
隔離性
隔離性,通常來說正在執行的事務對別的事務不可見的。只有事務執行完後提交結果,別的事務纔可見。
或許你已經注意到了我那幾個大大的紅字,這說明隔離性不是絕對的看不見。這個,下文會講清楚的。
假設有一天,小明賬戶餘額爲小明做了兩件事:轉1000塊給小丁和充話費。那麼作爲程序猿的你,意識到了這兩件事不是同一個 業務層實現的,你決定開啓兩個事務,分別爲 A 事務和 B 事務。
A 事務
開啓事務
數據庫執行扣費 sql,小明的賬號扣除1000
然後給小丁的賬戶加1000
提交事務
B 事務
開啓事務
購買充值服務
數據庫執行扣費 sql,小明的賬號扣除500
提交事務
數據庫事務併發問題
假設事務 A 和事務 B 在多併發事務下操作:
- 事務 A 修改了一些數據,然後事務 B 讀取到了,但是因爲某個原因,事務 A 回滾了,事務 B 相當於讀到了不存在的數據,稱爲髒讀。
- 事務 A 第一次從數據庫中讀取了一些數據,恰好事務 B 修改了這部分的數據,然後事務 A 再次讀取的時候,這部分數據已經改變了。說明了事務 A 對同樣的數據重複讀取會出現問題,這種稱爲不可重複讀。
- 事務 A 第一次從數據庫中讀取了某個範圍的記錄時,恰好事務 B 在這個範圍插入了新的數據,然後事務 A 再次讀取這個範圍的數據的時候,會發現比原來的多出新增的數據。這種行爲稱爲幻讀。
數據庫事務隔離級別
其實要求事務具有隔離性,實現起來是非常複雜的。因爲在實現不同隔離級別,數據庫引擎需要做更多的操作去保證這種特性例如加鎖等等。
但是其實並不是所有業務都需要需要最高級的隔離級別。這個權衡,其實是留給不同的業務做準備的。
下面介紹四種隔離級別
READ UNCOMMITED (未提交讀)
在這個級別當中,事務的修改,即使沒有提交,對其他事物是可見的。這個級別會導致一個問題,就是別的事務可能會讀到錯誤的數據,這稱爲髒讀。
READ COMMITED (提交讀)
一個事務開始時,只能“看見”已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事物都是不可見的。
這個隔離級別是滿足了 ACID
中隔離性的定義。
這個級別有時候也叫做不可重複讀,因爲兩次執行統一的查詢,可能會得到不一樣的結果。
REPEATABLE READ (可重複讀)
此隔離級別解決髒讀的問題。
保證了在同一個事務中多次讀取同樣的記錄的結果都是一致的。
但是理論上,可重複讀隔離級別還是無法解決另外一個幻讀的問題。
所謂的幻讀,指的是當某個事務在讀取某個範圍的記錄時,另外一個事務又在該範圍內插入新記錄,當之前的事務再次讀取該範圍的
記錄時,會出現幻行。InnoDB
和 XtraDB
通過引擎通過多版本併發控制,解決了幻讀的問題。
可重複讀是 MySQL 的默認事務隔離級別。
SERIABLIZABLE (可串行化)
SERIABLIZABLE 是最高的隔離級別。 它通過強制好事物串行執行,避免了前面說的幻讀的問題。
但是爲了絕對的數據一致性以及沒有併發的情況下,SERIABLIZABLE 會在讀取每一行數據都加上鎖,這樣會引起大量的超時已經鎖的爭用。
總結隔離級別及其解決的問題
隔離級別 | 髒讀可能性 | 不可重複讀可能性 | 幻讀可能性 | 加鎖讀 |
---|---|---|---|---|
READ UNCOMMITED | yes | yes | yes | no |
READ COMMITED | no | yes | yes | no |
REPEATABLE READ | no | no | yes | no |
SERIABLIZABLE | no | no | no | yes |
死鎖
死鎖是指兩個或多個事務在同一資源上相互佔用,並請求鎖定對象的請求。
事務 A
start transaction;
UPDATE student SET name = '章三' where id = 1;
UPDATE student SET name = '里斯' where id = 2;
commit;
事務B
start transaction;
UPDATE student SET name = '王五' where id = 2;
UPDATE student SET name = '老劉' where id = 1;
commit;
假如事務 A 和事務 B 都執行到了第一條 SQL,這是事務 A 更新了該行數據,並鎖定了該行。
當它嘗試去執行第二條數據的時候,會發現第二行數據被事務 B 佔有了。所以只有等待事務 B 釋放鎖。
同樣事務 B 也在等事務 A 釋放鎖。最後產生了死鎖。
數據庫引擎會通過非常多的途徑去檢測是否有死鎖。
不同的數據庫引擎實現的策略是不同的。
InnoDB
目前處理死鎖的方法是,將持有最少行排她鎖的事務進行回滾。
MySQL 中的事務
MySQL
直吹兩種事務型的存儲引擎: InnoDB
和 NDB Cluster
。
自動提交
設置 AUTOCOMMIT
變量啓用或禁用自動提交模式
SHOW VARIABLES LIKE 'AUTOCOMMIT';
set AUTOCOMMIT = 1;
設置隔離級別
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITED;
MVCC(多版本併發控制)
上面說了,MVCC 是用來解決幻讀問題的。所謂的 MVCC,其實就是通過保存數據在某個時間點的快照來實現的。也就是說,不管需要執行多長時間,每個事務看到的數據都是一致的。
根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的數據可能不一樣的。
不同的存儲引擎的 MVCC 實現不同,典型的有樂觀併發控制和悲觀併發控制。
我們來看看 InnoDB 的 MVCC。InnoDB 主要是通過每行記錄後面保存兩個隱藏的列來實現的。這兩個列,一個是用來存儲行的創建時間,另外一個是保存行的過期時間(或刪除時間)。
當然存儲的並不是實際的時間值,而是系統版本號。每開始一個新的事務,系統版本號會自動遞增。事務開始時刻的系統版本號會作爲事務的版本號,用來和查詢到的每行記錄的版本號進行比較。
下面是 REPEATABLE READ 隔離級別下,MVCC 具體是如何操作的。
SELECT
InnoDB 會根據以下兩個條件檢查每行記錄:
- InnoDB只查找版本早於當前事務版本的數據行(也就是數據行的版本必須小於等於事務的版本),這確保當前事務讀取的行都是事務之前已經存在的,或者是由當前事務創建或修改的行
- 行的刪除操作的版本一定是未定義的或者大於當前事務的版本號,確定了當前事務開始之前,行沒有被刪除
符合了以上兩點則返回查詢結果。
INSERT
InnoDB 爲每個新增行記錄當前系統版本號作爲創建ID
DELETE
InnoDB 爲每個刪除行的記錄當前系統版本號作爲行的刪除ID。
UPDATE
InnoDB 複製了一行。這個新行的版本號使用了系統版本號。它也把系統版本號作爲了刪除行的版本。
優缺點
僅僅多了兩個額外的系統版本號,使大多數的讀操作不用加鎖。這樣設計使得讀數據操作更簡單,性能更好,並且也會讀到符合標準的行。不足之處在於每行記錄都需要額外的空間,需要做更多的檢查工作,以及一些額外的維護工作。
MVCC 只在 REPEATABLE READ 和 READ COMMITTED 兩個隔離下工作。其他兩個隔離級別都和 MVCC 不兼容。因爲 READ UNCOMMITTED 總是讀取最新的數據行,而不是符合當前事務版本的數據行,而 SERIABLIZABLE 會對所有讀取的行都加鎖。