數據庫系列:MySQL慢查詢分析和性能優化
數據庫系列:MySQL索引優化總結(綜合版)
數據庫系列:高並發下的數據字段變更
數據庫系列:覆蓋索引和規避回表
數據庫系列:數據庫高可用及無損擴容
數據庫系列:使用高區分度索引列提升性能
數據庫系列:前綴索引和索引長度的取舍
數據庫系列:MySQL引擎MyISAM和InnoDB的比較
數據庫系列:InnoDB下實現高並發控制
1 事務概念和必要性
在MySQL中,事務是一個數據庫操作的最小執行單元,它由一個或多個SQL語句組成,這些SQL語句要么全部執行成功,要么全部失敗回滾。
所以,事務是一種機制,用來保證一系列操作要么全部執行成功,要么全部失敗回滾,從而保持數據庫的一致性和完整性。MySQL中只有使用支持事務的存儲引擎(如InnoDB)才能使用事務功能。
如果數據庫中沒有事務機制,那會怎么樣呢?
★ 超級典型的金融案例,案例改編自《高性能MySQL》第四版:
假設銀行對兩個用戶賬號進行轉賬:操作用戶賬戶表(包括轉賬源頭 和 轉賬目標)。現在要從用戶A的賬戶轉賬 1000 元到用戶B的賬戶中,那么需要至少三個步驟:
- 檢查賬戶A的余額高於 1000 元。
- 從賬戶A余額中減去 1000 元。
- 在賬戶B的余額中增加 1000 元。
上述三個步驟的操作必須打包在一個事務中,任何一個步驟失敗,則必須回滾所有的步驟。
可以用 START TRANSACTION 語句開始一個事務,然后要么使用 COMMIT 提交事務將修改的數據持久保留,要么使用 ROLLBACK 撤銷所有的修改。
事務SQL的樣本如下:
/* 開始事務 */
START TRANSACTION;
/* 檢查賬戶A(123456)的余額高於 1000 元 */
SELECT balance FROM acount WHERE customer_id=123456;
/* 從賬戶A(123456)余額中減去 1000 元 */
UPDATE acount SET balance=balance-1000.00 WHERE customer_id=123456;
/* 在賬戶B(123457)余額中增加 1000 元 */
UPDATE acount SET balance=balance+1000.00 WHERE customer_id=123457;
/* 提交事務 */
COMMIT;
解讀下這個SQL腳本:
- 如果執行到第四條語句時服務器崩潰了,用戶A可能會損失1000元,而用戶B也沒有接收到1000元。
- 如果執行到第三條語句和第四條語句之間時,另外一個進程要消費掉A賬戶的所有余額,那么結果可能就是銀行在不知道這個邏輯的情況下白白給了B賬戶1000元。
所以,金融類系統需要有嚴格的ACID測試,ACID是指原子性 (atomicity)、一致性(consistency)、隔離性(isolation)和持久性durability)。一個運行良好的事務處理系統,必須具備這些標准特征。
2 事務的四個特性(ACID)
一般來說,衡量事務必須滿足四個特性:ACID,即 原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。
- 原子性(Atomicity):一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- 一致性(Consistency):在事務開始之前和事務結束以后,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及后續數據庫可以自發性地完成預定的工作。
- 隔離性(Isolation):數據庫允許多個並發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交(read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable),下面會詳細說明。
- 持久性(Durability):事務處理結束后,對數據的修改就是永久的,會持久化到硬盤上,即便系統故障也不會丟失。
3 如何保證事務的隔離性
3.1 數據庫並發下事務的三種現象
3.1.1 臟讀
讀取事務未提交數據
臟讀就是指當一個事務A正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務B也訪問這個數據,讀了未提交事務操作的數據,然后使用了這個臟數據。舉個例子:
時間序列 | A事務 | B事務 |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢B賬戶有100元余額★SELECT balance FROM acount WHERE customer_id=123456; |
|
T4 | B賬戶增加1000元轉賬(未提交)★UPDATE acount SET balance=balance+1000.00 WHERE customer_id=123457; |
|
T5 | 查詢B賬戶有1100元余額(讀臟數據) | |
T6 | 入賬失敗,回滾1000元轉賬款 | |
T7 | 提交事務★commit; |
3.1.2 不可重復讀
前后多次讀取數據不一致
不可重復讀指的是在事務A中先后多次讀取同一個數據,讀取的結果不一樣,因為另外一個事務也訪問該同一數據,並且可能修改這個數據,這種現象稱為不可重復讀。
臟讀與不可重復讀的區別在於:前者讀到的是其他事務未提交的數據,后者讀到的是其他事務已提交的數據。
時間序列 | A事務 | B事務 |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢B賬戶有100元余額★SELECT balance FROM acount WHERE customer_id=123456; |
|
T4 | B賬戶增加1000元轉賬(未提交)★UPDATE acount SET balance=balance+1000.00 WHERE customer_id=123457; |
|
T5 | 提交事務★commit; |
|
T6 | 查詢B賬戶有1100元余額(不可重復讀) |
按照正確邏輯,事務B前后兩次讀取到的數據應該一致,這邊一次讀到的是100元,一次讀到的是1100元,得到了不同的結果。
3.1.3 幻讀
前后多次讀取,數據不一致
在事務A中按照某個條件先后兩次查詢數據庫,兩次查詢結果的條數不同,這種現象稱為幻讀。不可重復讀與幻讀的區別可以通俗的理解為:前者是數據變了,后者是數據的行數變了。通俗點就是已提交事務B對事務A產生的影響,導致B執行有誤,這個影響叫做“幻讀”。
時間序列 | A事務 | B事務 |
---|---|---|
T1 | 開始事務 | 開始事務 |
T2 | 第一次查詢數據庫賬戶表有2條數據,鍵 pay_id是1和2 | |
T3 | 給賬戶表轉賬1000元,所以新增一條 pay_id為3的轉賬數據 | |
T4 | 提交事務成功 | |
T5 | 因為上面查到的pay_id=2, 所以這邊新增一條pay_id=3的消費數據,insert的時候提示key沖突 |
按照正確邏輯,事務B前后兩次讀取到的數據總量應該一致
3.14 不可重復讀和幻讀的區別
-
不可重復讀是讀取了其他事務更改的數據,針對update操作
★ 解決:使用行級鎖,鎖定該行,事務A多次讀取操作完成后才釋放該鎖,這個時候才允許其他事務更改剛才的數據。 -
幻讀是讀取了其他事務新增的數據,針對insert與delete操作
★ 解決:使用表級鎖,鎖定整張表,事務A多次讀取數據總量之后才釋放該鎖,這個時候才允許其他事務新增數據。
幻讀和不可重復讀都是指的一個事務范圍內的操作受到其他事務的影響了。只不過幻讀是重點在插入和刪除,不可重復讀重點在修改
所以,從上面的幾個案例可以看到,並發的事務可能導致其他事務的問題包括:
- 讀臟數據(最后事務並未提交成功)
- 不可重復讀
- 幻讀
如何解決,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致。事務隔離分為不同級別,包括
- 讀未提交(read uncommitted)
- 讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable)
3.2 事務的隔離級別
SQL92標准中事務的隔離性(Isolation)定義了四種隔離級別,並規定了每種隔離級別下上述幾個(臟讀、不可重復讀、幻讀)問題是否被解決。
一般來說,隔離級別越低,系統開銷越低,可支持的並發越高,但隔離性也越差。隔離級別與數據庫讀的3個問題的關系如下:
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|
讀未提交:Read Uncommitted | ✔ | ✔ | × |
讀已提交:Read Committed | × | ✔ | × |
可重復讀:Repeatable Read | × | × | ✔ |
串行化:Serializable | × | × | × |
不同事務的隔離級別,實際上是一致性與並發性的一個權衡與折衷,它本質上也是InnoDB不同的鎖策略(Locking Strategy)產生的效果 。接下來我們對這幾種事務隔離機制詳細介紹下:
3.2.1 讀未提交(Read Uncommitted)
讀未提交情況下,可以讀取到其他事務還未提交的數據,多次讀取結果不一樣,出現了臟讀、不可重復讀的情況。
這種情況下select語句不加鎖:SELECT statements are performed in a nonlocking fashion.
,所以這是並發最高,一致性最差的隔離級別。
3.2.2 讀已提交(Read Committed)
這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。讀已提交情況下,無法讀取到其他事務還未提交的數據,可以讀取到其他事務已經提交的數據,多次讀取結果不一樣,不會出現臟讀和幻讀,但出現不可重復讀。
這種隔離級別用的比較多,也是互聯網業務最常用的隔離級別,在Read Committed 下:
- 普通讀是快照讀取
- 加鎖的select, update, delete等語句,除了在外鍵約束檢查(foreign-key constraint checking)以及重復鍵檢查(duplicate-key checking)時會封鎖區間,其他時刻都只使用記錄鎖;
3.2.3 串行化(Serializable)
這種事務的隔離級非常嚴格,在這種串行情況下不存在臟讀、不可重復讀、幻讀的問題了。
所有select請求語句都會被隱式的轉化為:
select ... in share mode.
這可能導致,如果有未提交的事務正在修改某些行,所有讀取這些行的select都會被阻塞住,直到之前的事務執行完成。
這是一致性最好的,但並發性最差的隔離級別。執行順序參考如下:
時間 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(9,'初三九班'); | |
T5 | select * from classes; | |
T6 | commit; | |
T7 | commit; |
這個明顯效率太慢了,在大數據量,大並發量的互聯網場景下,基本上是不會使用上述這種隔離級別。
3.2.4 可重復讀(Repeated Read, RR)
RR(可重復讀)是InnoDB默認的隔離級別,在R這種隔離級別下:
-
select操作使用快照讀(snapshot read),這是一種不加鎖的一致性讀(Consistent Nonlocking Read),底層使用MVCC來實現,MVCC 參考作者的這篇:數據庫系列:InnoDB下實現高並發控制
-
加鎖的
select(select ... in share mode / select ... for update), update, delete
等語句條件的實現,其實是依賴於它們是否在唯一索引(unique index)上使用,如:- 唯一的查詢條件(unique search condition)
- 范圍查詢條件(range-type search condition)
可重復讀情況下,未出現臟讀,未讀取到其他事務已提交的數據,多次讀取結果一致,即可重復讀。但是可能導致“幻讀”。
4 總結
- 認識ACID(原子性、一致性、隔離性、持久性)特性及其實現原理
- 了解事務的臟讀、幻讀、不可重復讀
- 了解InnoDB實現deSQL92標准中的四種隔離級別
- InnoDB默認的隔離級別是RR,互聯網用得最多的隔離級別是RC,它解決了臟讀和幻讀,保證了數據隔離性和一致性。