本文首發於公眾號:Hunter後端
原文連結:MySQL面試必備三之事務
這一篇筆記介紹一下 MySQL 的事務,面試中常被問到關於事務的幾個問題如下:
- 事務是什麼
- 為什麼需要事務,事務有什麼作用
- 事務的特點
- 事務可能帶來哪些問題
- 事務有哪些隔離級別,這些隔離級別都可以解決哪些問題
- 可重複讀隔離級別下能否解決幻讀問題
- 如何解決幻讀問題
以下是本篇筆記目錄:
- 什麼是事務
- 事務的特性
- 事務執行的示例
- 併發事務可能帶來的問題
- 事務的隔離級別
- 可重複讀隔離級別為什麼不可以解決幻讀的問題
1、什麼是事務
所謂事務,就是一系列的 SQL 組合,這些 SQL 操作要麼全部執行,要麼都不執行,是一個不可分割的工作單位。
比如我們在支付系統中想要完成一個轉賬功能,比如從 A 賬戶轉賬一百元給 B 賬戶,那麼從 A 賬戶的總額中減去 100,然後在 B 賬戶上加上 100,這兩個就需要全部執行才算是這個轉賬操作的實現。
這個過程就包含了 A 賬戶的減少 100,B 賬戶的加上 100,這兩個操作加起來就是一個完整的事務。
2、事務的特性
事務的特性有四個,為ACID,分別是 A(Atomicity)、C(Consistency)、I(Isolation) 和 D(Duration),分別表示原子性、一致性、隔離性和永續性。
1. 原子性
事務的原子性指的是一個事務中的所有操作要麼全部完成,要麼全部失敗,如果在執行事務的過程中,某個 SQL 執行失敗,那麼這個事務中之前執行操作全部回滾,恢復到執行事務之前的狀態。
2. 一致性
一致性指的是事務執行前後,資料庫的狀態應該保持一致,即資料庫的完整性不會被破壞。
這個一致性的理解為在執行事務前後資料庫應該符合事務的約束條件,從而保證資料的正確性。
比如我們設定了某個欄位的屬性應當大於或等於 0,但在某個操作過程中如果更新該欄位的值小於 0,那麼則屬於破壞了資料的一致性,事務會回滾到執行前,從而保證資料庫狀態的一致性。
3. 隔離性
隔離性指的是多個事務併發執行時,每個事務都應該獨立於其他事務,互不干擾,從而避免資料併發訪問引起的問題。
事務的隔離分為多個級別,這個在後面再介紹。
4. 永續性
事務的永續性指的是事務執行完畢之後,對資料的修改就是永久的,即便是系統故障,修改的資料也不會丟失。
3、事務執行的示例
事務的執行過程會包含幾個步驟,事務的開始、SQL 操作、提交或者回滾。
1. 事務執行示例
比如我們想要給 id=1
的賬號減去一百元,然後給 id=2
的賬號加上一百元,使用事務來操作的示例如下:
START TRANSACTION;
UPDATE user_account set money = money - 100 WHERE id = 1;
UPDATE user_account set money = money + 100 WHERE id = 2;
COMMIT;
這裡,我們透過 START TRANSACTION
開啟一個事務,中間執行 SQL 操作,以 COMMIT
提交事務為結束。
2. 回滾操作
如果我們想執行回滾操作,可以直接使用 ROLLBACK
:
START TRANSACTION;
UPDATE user_account set money = money - 100 WHERE id = 1;
UPDATE user_account set money = money + 100 WHERE id = 2;
ROLLBACK;
這裡的回滾操作會回滾到事務執行前。
3. 儲存點
如果我們的事務包含的 SQL 很長,我們並不想直接回滾到事務開啟前,而是事務中間的某個步驟,我們可以使用儲存點來進行回滾操作:
-- 開始事務
START TRANSACTION;
-- 執行 SQL 操作
UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
SAVEPOINT before_insert;
INSERT INTO transaction_log (account_id, amount, type) VALUES (123, 100, 'debit');
-- 檢查條件
IF some_condition THEN
-- 回滾到儲存點
ROLLBACK TO before_insert;
ELSE
-- 提交事務
COMMIT;
END IF;
在這裡,我們透過 SAVEPOINT
來建立儲存點,並在後面的程式碼裡透過 IF 條件進行判斷,選擇性的回到該儲存點。
一個事務是可以包含多個儲存點的。
4. 單條 SQL 的事務
前面幾條介紹的都是使用 START TRANSACTION
顯式地開始一個事務,而至於單條 SQL 語句,比如 INSERT 或者 UPDATE 這種,在預設情況下是自動提交的,所以不用手動進行 COMMIT 操作,它們也屬於單獨的事務。
4、併發事務可能帶來的問題
在我們訪問資料庫時,可能同一時刻有多個事務在訪問運算元據庫,那麼這樣可能會導致一些問題。
1. 髒讀
所謂髒讀就是在在某個事務的執行過程中可以讀到其他事務未提交的資料,這個現象就是髒讀。
因為一個事務的執行是可能包含多個 SQL 的,在某種事務隔離級別下就可能存在 A 事務執行了 SQL 但是還未提交,這時候 B 事務執行過程中就讀取到了 A 事務更改的資料。
2. 不可重複讀
不可重複讀的現象指的是在同一個事務中,有兩個讀取資料的 SQL,這兩次讀取的資料內容都不一樣,這種現象就稱為不可重複讀。
這個現象產生的原因在於這兩次讀取 SQL 的過程中,有其他事務更新了這條資料並提交了。
不可重複讀偏重的點在於對資料的修改。
3. 幻讀
幻讀的現象指的是在同一個事務中,兩次查詢資料返回的結果的條數不一樣,它產生的原因同樣是兩次查詢期間有其他事務提交了,但它的側重點是其他事務是對資料的插入或者刪除。
5、事務的隔離級別
事務的隔離級別分別是讀未提交(Read Uncommited)、讀已提交(Read Commited)、可重複讀(Repeatable Read)、序列化(Serializable)。
1. 讀未提交
讀未提交指的是一個事務可以讀取到其他事務未提交的內容。
在這個隔離級別下,如果有一個事務 A,包含多條 SQL 操作,執行到其中某條 SQL,但是還沒有執行 COMMIT 操作,這個時候另一個事務 B 讀取事務 A 操作過的 SQL 資料,就可以讀取到對應內容,這個過程就是讀未提交。
讀未提交這個隔離級別可能會造成資料的髒讀問題。
2. 讀已提交
讀已提交指的是事務可以讀取到其他事務已經提交的資料,這個隔離級別可以解決髒讀問題,但是不可以解決不可重複讀和幻讀的問題。
比如一個事務 A,在其執行過程中先讀取了某條資料,這個時候另一個事務 B 開啟一個事務並提交,事務 B 修改了事務 A 前面讀取的資料內容,這個時候事務 A 在後面的操作又讀取了這條資料,會發現和第一次讀的時候資料不一致,這個就是讀已提交可能造成的問題。
3. 可重複讀
可重複讀則是在事務開始的時候會先獲取一個當前時刻資料的快照,並且在整個事務的過程中都會從這個快照中讀取資料,這個就是可重複讀。
可重複讀可以解決不可重複讀的問題,因為不可重複讀針對的是某條資料本身,而可重複讀會對資料本身做快照處理,所以可以解決不可重複讀的問題。
而為什麼不可以解決幻讀的問題呢?
這個我們後面再介紹。
4. 序列化
序列化是最高的隔離級別,在這個隔離級別下,會將分別對讀操作和寫操作加鎖,當一個事務正在執行,其他事務必須等前一個事務執行完畢之後才能執行。
在這個隔離級別下,可以解決前面併發事務帶來的所有問題,包括不可重複讀和幻讀,但同時,這種方式也會降低資料庫的併發效能,因為事務需要按照其他事務釋放鎖才能執行。
注意:MySQL 預設的隔離級別是可重複讀。
6、可重複讀隔離級別為什麼不可以解決幻讀的問題
接著來說一說為什麼可重複讀隔離級別不可以解決幻讀的問題。
在說明這個問題的原因之前,先來介紹一下快照讀和當前讀。
1. 快照讀和當前讀
1) 快照讀
快照讀指的是在事務開始的時候,事務會建立一個資料的快照,在接下來這個事務的整個過程中,都會使用這個快照來讀取資料。
2) 當前讀
當前讀則是指在讀取資料時,直接讀取庫裡最新的資料,而不使用事務開啟時建立的快照資料。
2. 使用快照讀和當前讀的場景
在 MySQL 中,除了普通的 SELECT
查詢語句是快照讀,UPDATE、INSERT、DELETE 操作都是當前讀,也就是對資料進行更新、插入和刪除的時候都是會查詢到資料庫最新的資料然後進行操作。
除此之外,對 SELECT 操作進行加鎖操作也是當前讀,比如共享鎖 select ... lock in share mode
和排他鎖 select ... for update
,這個我們後面再介紹。
3. 可重複讀隔離級別下的幻讀操作
我們可以透過下面一個例子來進行闡述,在可重複讀隔離級別下幻讀操作是如何產生的。
比如有兩個事務,分別是事務 A 和事務 B,A 事務開啟後,查詢資料庫中的資料,這個時候事務 B 開啟,並且使用 INSERT 插入一條資料並提交,在這之後,A 事務對資料庫中的資料進行一個 UPDATE 全量資料的操作,之後再進行一個 SELECT 的操作。
下面的程式碼示例,我們用前面的 t 序號作為執行的時間:
create table users (
id int not null auto_increment primary key,
name varchar(20) not null
);
INSERT INTO users (id, name) values(1, "張三");
-- t1 開啟事務A
START TRANSACTION;
-- t2 查詢資料
SELECT * from users;
-- t6 查詢資料
SELECT * from users;
-- t7 更新全部資料
UPDATE users SET name = "王五" WHERE id >= 1;
-- t8 查詢資料
SELECT * FROM users;
-- t9 提交事務A
COMMIT;
-- t3 開啟事務B
START TRANSACTION;
-- t4 事務B插入一條資料
INSERT INTO users (id, name) values(2, "李四");
-- t5 提交事務B
COMMIT;
對於上面的程式碼,我們分別開啟兩個 MySQL 終端然後按照時間順序執行,可以看到以下輸出:
事務 A 的整體操作如下:
事務 B 的整體操作如下:
可以看到事務 A 在第二次進行查詢的時候資料就會新增一條,和第一次查詢的時候資料不一致了,這個過程就產生了幻讀。
4. 如何解決幻讀問題
前面介紹了即便是可重複讀隔離級別下,也還是會可能產生幻讀問題,那麼如何解決幻讀問題呢,本質上還是加鎖,比如在查詢資料的時候使用 select for update
操作對查詢的資料加上間隙鎖,這樣就可以避免其他事務插入新的資料,關於鎖的概念和使用,我們在後面再詳細介紹。
如果想獲取更多相關文章,可掃碼關注閱讀: