文章目錄
一、事務是什麼
先來看第一個問題:什麼是事務(Transaction
)?
事務就是執行一組 SQL 語句。這些 SQL 語句就是一條繩上的螞蚱,要麼一起成功(Commit
),要麼一起失敗(RollBack
)。
- 比如用戶 A 向用戶 B 轉賬 10 元;
- 如果 A 先轉出了 10 元,但是在 B 收到之前服務器掛了,就會造成 A 的錢沒了,B 的錢也沒到賬;
- 如果把這兩條語句封裝成事務的話,就不會出現上述情況,而是整個過程執行失敗,退回 A 的錢結束事務。
事務的特性:
Atomicity
(原子性):像原子一樣不可分割;Consistency
(一致性):數據庫永遠保持一致;Isolation
(隔離性):事務之間是相互隔離的;Durability
(持久性):事務一旦提交,它的修改就是永久的。
這就是 ACID
。
下面來看一個栗子:
-- 開啓一個事務
start transaction;
insert into orders(customer_id, order_date, status)
values (1, '2020-05-10', 1);
insert into order_items
values (last_insert_id(), 1, 1, 1);
-- 提交就代表結束事務了
commit;
-- 回滾事務
rollback;
其中被transaction
和commit
包裹着的語句就是事務,也可以使用 rollback
包裹,這樣的話之前的修改就全部失效。
在 MYSQL
中,一條 SQL 語句默認就是一個事務:
# 每一個 SQL 語句都被設置成自動提交
show variables like 'autocommit';
二、爲什麼使用隔離級別
然後是第二個問題:爲什麼會有事務的隔離級別?
既然已經有了事務,轉賬啥的問題也都挺好的解決了,那隔離級別優勢啥玩意?
什麼是併發
在瞭解這個之前我們先來介紹一下什麼是併發:
- 併發就是多個用戶訪問數據庫,同時對相同的數據進行修改帶來的問題。
- 比如用戶 A 創建了事務,還沒來得及提交,用戶 B 搶先提交了,關鍵是他們修改的是同一個事務,那麼到底應該聽誰的呢?
1、默認併發處理
那麼 MySQL 默認是怎麼處理併發的呢?
- MySQL 默認會讓其中一個事務等待另一個事務先執行完成再執行該事務。
- 他會先將數據鎖定,就是給行或者數據加鎖,確保在第一個事務執行完成之前,數據不會被其他事務修改。
- 所以在默認的情況下是不用擔心事務的併發問題的。
2、常見併發問題
但是默認並不是萬能的,總有一些問題是它解決不來的,接下來我們就來看一下這些問題。
Lost Updates
第一個問題是丟失更新
。
什麼時候會出現?
兩個事物試圖更新相同的數據而我們不使用鎖時,就會出現這種情況。
造成的結果:後面提交的數據會覆蓋掉前面的提交。
但是這種情況 MySQL 會默認處理的,即放在隊列中依次執行,所以一般我們不用考慮。
Dirty Reads
第二個問題就是髒讀
。
什麼時候會出現?
A 事務讀取還沒有提交時:另一個事務 B 卻將 A 的還沒有提交的數據當成了真實存在的數據,如果 A 最後提交了還好,如果 A 回滾了,那麼 B 中的數據就是假的,也就是髒的,所以叫髒讀。
造成的結果:讀取了還未提交的數據,數據是髒的。
Non-repeating Reads
第三個問題是不可重複讀
。
什麼時候會出現?
事務 A 執行子查詢,在外層查詢中先獲取指定的值,這個時候事務 B 過來修改了 A 剛纔讀的數據,但是之後 A 的子查詢再次讀了這個數據而這個數據兩次讀到的值不一樣了,這就是不可重複讀。
造成的結果:兩次讀取的內容可能會不同。
Phantom Reads
第四個問題是幻讀
。
什麼時候會出現?
在事務 A 中使用 where
條件查詢,此時事務 B 修改了原來符合條件的數據,使得它現在不符合條件了,或者說增加了幾個符合條件的數據,但是事務 A 始終讀取的都是原來的數據。
造成的結果:丟失符合條件的某些行,就很玄幻,所以是幻讀。
三、怎麼做才能解決併發問題
那怎麼解決這些併發問題呢? 答案是使用隔離級別對事務進行隔離。
該圖左邊是事務的隔離等級,右邊是這個隔離等級可以解決的問題。標準的 SQL 定義了四個隔離級別,他們分別是:
Read Uncommitted
,不提交讀:他什麼問題也解決不了;Read Committed
,提交讀:可以解決髒讀,隔離級別是一次讀取的範圍,所以無法解決一次事務兩次讀取中間可能出現的問題;Repeatable Read
,可重複讀:可以解決前三個問題,可以解決大部分的問題 ;Serializable
,序列化:可以解決所有問題,他會等待其他事務執行完畢之後再執行。
有一些注意點:
- MySQL 默認是可重複讀的隔離等級,之前我們也提到了這個默認情況。
- 隔離界別越高,性能損耗越大,所以要權衡利弊選擇隔離級別。
好,我們現在來回顧總結一下:
# 多個用戶同時修改同一個數據稱爲併發, MYSQL 會自動鎖定 update 的內容
# 事務帶來的常見的問題
/*
- 1.丟失更新: 兩個事物同時修改數據,後面提交的數據會覆蓋掉前面的提交
- 2.髒讀(無效數據讀取): 讀取到未提交的數據
A事務讀取還沒有提交時-另一個事務B卻將A的還沒有提交的數據當成了真實存在的,如果A最後提交了還好,如果A回滾了,那麼B中的數據就是假的;
即讀取了還未提交的數據,數據是髒的;
-- 爲了解決這個問題,我們可以提供一定的隔離度,即事務修改的數據不會立即被其他事物看到,除非他已經提交了
-- 標準的 SQL 定義了四個隔離級別 - 針對髒讀的就是 提交讀 (Read Committed) 只能讀取已經提交的數據
- 3.不可重複讀取: 兩次讀取的內容可能會不同
事務A執行子查詢,在外層查詢中先獲取指定的值,這個時候事務B過來修改了A剛纔讀的數據,但是之後A的子查詢再次讀了這個數據
而這個數據兩次讀到的值不一樣了.這就是不可重複讀
-- 這個時候應該增加隔離級別: 可重複讀,達到這個目的 -> 以最開始的那個數據爲準,不受事務B修改的影響,隔離級別是事務的級別.
- 4.幻讀: 丟失符合條件的某些行
在事務A中使用 where 查詢,此時事務 B 修改了原來符合條件的數據,使得它現在不符合條件了.
需要的隔離等級: 序列化 Serializable 等級,使該事務可以知道其他事物正在修改數據.
這樣他就會等待其他事務修改完之後纔會執行事務,缺點就是如果併發比較多,執行會很慢
等級越高,性能消耗越大;
MySQL默認是可重複讀的隔離等級;
*/
大部分情況下都保持默認,只在特殊情況下修改隔離等級。
如何設置隔離等級?
-- 查看隔離等級
show variables like 'transaction_isolation';
-- 設置隔離等級
set transaction isolation level SERIALIZABLE ;
-- 在當前會話中設置
set session transaction isolation level serializable ;
四、MySQL 中的鎖
數據庫遵循的是兩段鎖協議,將事務分爲兩個階段:
- 加鎖階段;
- 加鎖階段只允許事務加鎖,在對任何事務進行
讀操作
之前一定要先申請並獲得S鎖
(共享鎖) ,進行寫操作
之前要申請並獲得X鎖
(排它鎖),如果獲得失敗,即加鎖不成功則必須等待,直到加鎖成功才繼續執行。
- 加鎖階段只允許事務加鎖,在對任何事務進行
- 解鎖階段;
- 當事務釋放了一個封鎖之後,事務進入解鎖階段,該階段只能進行解鎖操作而不能進行加鎖操作。
舉一個栗子:
MySQL 中的鎖分爲行鎖
和表鎖
,表鎖通常是將整個的一張表鎖住,會降低併發處理能力,所以一般只在 DDL
(數據庫定義語言) 中使用。這裏主要討論行鎖:
1. Read Committed
在 RC
級別中,數據的查詢是不需要加鎖的,但是數據的增刪改是需要加鎖的。
如果事務一直得不到鎖則會一直等待,直到 wait
超時。
如果條件語句沒有索引,MySQL 會將該表中的所有數據行加行鎖。
但是在實際的應用中,會在 MySQL Server 中進行過濾,調用unlock_row
方法,從而將無關的數據項解鎖(但是這違背了兩段鎖協議)。
對一個數據量很大的表做批量修改的時候,如果無法使用相應的索引,MySQL Server 過濾數據的的時候特別慢,就會出現雖然沒有修改某些行的數據,但是它們還是被鎖住了的現象。
2. Repeatable Read
RR
解決了在一次事務中,兩次讀取的內容不一致。
那麼他是怎麼做得到呢?
2.1 不可重複讀和幻讀的區別
這裏穿插出一個知識點,也是大家容易記混的地方。
- 不可重複讀重點在於
update
和delete
; - 而幻讀的重點在於
insert
。
幻讀與不可重複讀類似,幻讀是查詢到了另一個事務已提交的新插入數據,而不可重複讀是查詢到了另一個事務已提交的更新數據。
簡單來說,不可重複讀是由於數據修改引起的,幻讀是由數據插入或者刪除引起的。
在可重複讀中,該
sql
第一次讀取到數據後,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重複讀了。
但這種方法卻無法鎖住 insert
的數據,所以當事務 A 先前讀取了數據,或者修改了全部數據,事務 B 還是可以 insert
數據提交,這時事務 A 就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。
需要 Serializable
隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這麼做可以有效的避免幻讀、不可重複讀、髒讀等問題,但會極大的降低數據庫的併發能力。
但是 MySQL 使用了以樂觀鎖
爲理論基礎的MVCC
(多版本併發控制)來避免這兩種問題。
2.2 悲觀鎖和樂觀鎖
悲觀鎖
指的是對於數據被外界修改持保守態度,在整個數據處理過程中,將數據處於鎖定狀態,它依賴於數據庫底層提供的鎖機制,讀取數據時加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。Serializable
就是使用的悲觀鎖。
樂觀鎖
採取更加寬鬆的策略,相對於悲觀鎖的高性能開銷樂觀鎖性能很好。大多是基於數據版本記錄
機制實現的。一般就是在數據表中增加一個字段 version
字段,讀取數據時將此版本號一併讀出,之後每更新一次,版本號加一。然後將提交的數據版本信息與數據表記錄的那個版本信息進行比對,如果大於等於則更新原來的版本號,否則認爲是過期數據。
很多文章都說 RR
級別是可重複讀的,但無法解決幻讀,而只有在 Serializable
級別才能解決幻讀。但是其實 RR
級別是可以解決幻讀的讀問題的。這得益於樂觀鎖的實現,即 MVCC 。
2.3 “讀”與“讀”的區別
MySQL
中的讀,和事務隔離級別
中的讀,是不一樣的。
有的時候我們讀取到的數據是歷史數據,是不及時的,這種我們叫它 快照讀
,而讀取數據庫當前版本數據的方式叫做 當前讀
。
- 快照讀:就是
select * from table ….
- 當前讀:特殊的讀操作,
插入/更新/刪除
操作,屬於當前讀,處理的都是當前的數據,需要加鎖。
事務的隔離級別實際上就是定義了當前讀
的級別,MySQL
爲了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select
不用加鎖。
2.4 Next-Key 鎖
MySQL 使用了 Next-Key 鎖來解決當前讀
中的幻讀問題(也就是寫)。
Next-Key 鎖是行鎖
和GAP
(間隙鎖)的合併。
那麼 GAP 鎖又是什麼呢?
RR
級別中,事務 A 在 update
後加鎖,事務 B 無法插入新數據,這樣事務 A 在 update
前後讀的數據保持一致,就避免了幻讀。這個鎖,就是Gap鎖。
行鎖防止別的事務修改或刪除,
GAP
鎖防止別的事務新增,行鎖
和GAP鎖
結合形成的的Next-Key
鎖共同解決了RR
級別在寫數據時的幻讀問題。
3. Serializable
這個級別很簡單,讀加共享鎖,寫加排他鎖,讀寫互斥。使用的悲觀鎖的理論,實現簡單,數據更加安全,但是併發能力非常差。如果你的業務併發的特別少或者沒有併發,同時又要求數據及時可靠的話,可以使用這種模式。
參考資料:
- MySQL參考手冊
- Innodb中的事務隔離級別和鎖的關係
- 《高性能MySQL》中文第三版 P181