事務,由一個有限的資料庫操作序列構成,這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。
假如A轉賬給B 100 元,先從A的賬戶里扣除 100 元,再在 B 的賬戶上加上 100 元。如果扣完A的100元后,還沒來得及給B加上,銀行系統異常了,最後導致A的餘額減少了,B的餘額卻沒有增加。所以就需要事務,將A的錢回滾回去,就是這麼簡單。
為什麼要有事務呢? 就是為了保證資料的最終一致性。
事務四個典型特性,即ACID,原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability)。
事務並行會引起髒讀、不可重複讀、幻讀問題。
如果一個事務讀取到了另一個未提交事務修改過的資料,我們就稱發生了髒讀現象。
假設現在有兩個事務A、B:
因為事務A讀取到事務B未提交的資料,這就是髒讀。
同一個事務內,前後多次讀取,讀取到的資料內容不一致
假設現在有兩個事務A和B:
事務A被事務B干擾到了!在事務A範圍內,兩個相同的查詢,讀取同一條記錄,卻返回了不同的資料,這就是不可重複讀。
如果一個事務先根據某些搜尋條件查詢出一些記錄,在該事務未提交時,另一個事務寫入了一些符合那些搜尋條件的記錄(如insert、delete、update),就意味著發生了幻讀。
假設現在有兩個事務A、B:
事務A查詢一個範圍的結果集,另一個並行事務B往這個範圍中插入新的資料,並提交事務,然後事務A再次查詢相同的範圍,兩次讀取到的結果集卻不一樣了,這就是幻讀。
為了解決並行事務存在的髒讀、不可重複讀、幻讀等問題,資料庫大叔設計了四種隔離級別。分別是讀未提交,讀已提交,可重複讀,序列化(Serializable)。
讀未提交隔離級別,只限制了兩個資料不能同時修改,但是修改資料的時候,即使事務未提交,都是可以被別的事務讀取到的,這級別的事務隔離有髒讀、重複讀、幻讀的問題;
讀已提交隔離級別,當前事務只能讀取到其他事務提交的資料,所以這種事務的隔離級別解決了髒讀問題,但還是會存在重複讀、幻讀問題;
可重複讀隔離級別,限制了讀取資料的時候,不可以進行修改,所以解決了重複讀的問題,但是讀取範圍資料的時候,是可以插入資料,所以還會存在幻讀問題;
事務最高的隔離級別,在該級別下,所有事務都是進行序列化順序執行的。可以避免髒讀、不可重複讀與幻讀所有並行問題。但是這種事務隔離級別下,事務執行很耗效能。
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
讀未提交 | √ | √ | √ |
讀已提交 | × | √ | √ |
可重複讀 | × | × | √ |
序列化 | × | × | × |
資料庫是通過加鎖,來實現事務的隔離性的。這就好像,如果你想一個人靜靜,不被別人打擾,你就可以在房門上加上一把鎖。
加鎖確實好使,可以保證隔離性。比如序列化隔離級別就是加鎖實現的。但是頻繁的加鎖,導致讀資料時,沒辦法修改,修改資料時,沒辦法讀取,大大降低了資料庫效能。
那麼,如何解決加鎖後的效能問題的?
答案就是,MVCC多版本並行控制!它實現讀取資料不用加鎖,可以讓讀取資料同時修改。修改資料時同時可讀取。
MVCC,即Multi-Version Concurrency Control (多版本並行控制)。它是一種並行控制的方法,一般在資料庫管理系統中,實現對資料庫的並行存取,在程式語言中實現事務記憶體。
通俗的講,資料庫中同時存在多個版本的資料,並不是整個資料庫的多個版本,而是某一條記錄的多個版本同時存在,在某個事務對其進行操作的時候,需要檢視這一條記錄的隱藏列事務版本id,比對事務id並根據事物隔離級別去判斷讀取哪個版本的資料。
資料庫隔離級別讀已提交、可重複讀 都是基於MVCC實現的,相對於加鎖簡單粗暴的方式,它用更好的方式去處理讀寫衝突,能有效提高資料庫並行效能。
事務每次開啟前,都會從資料庫獲得一個自增長的事務ID,可以從事務ID判斷事務的執行先後順序。這就是事務版本號。
對於InnoDB儲存引擎,每一行記錄都有兩個隱藏列trx_id、roll_pointer,如果表中沒有主鍵和非NULL唯一鍵時,則還會有第三個隱藏的主鍵列row_id。
列名 | 是否必須 | 描述 |
---|---|---|
row_id | 否 | 單調遞增的行ID,不是必需的,佔用6個位元組。 |
trx_id | 是 | 記錄操作該資料事務的事務ID |
roll_pointer | 是 | 這個隱藏列就相當於一個指標,指向回滾段的undo紀錄檔 |
undo log,回滾紀錄檔,用於記錄資料被修改前的資訊。在表記錄修改之前,會先把資料拷貝到undo log裡,如果事務回滾,即可以通過undo log來還原資料。
可以這樣認為,當delete一條記錄時,undo log 中會記錄一條對應的insert記錄,當update一條記錄時,它記錄一條對應相反的update記錄。
undo log有什麼用途呢?
多個事務並行操作某一行資料時,不同事務對該行資料的修改會產生多個版本,然後通過回滾指標(roll_pointer),連成一個連結串列,這個連結串列就稱為版本鏈。如下:
其實,通過版本鏈,我們就可以看出事務版本號、表格隱藏的列和undo log它們之間的關係。我們再來小分析一下。
update core_user set name ="曹操" where id=1
,會進行如下流程操作快照讀: 讀取的是記錄資料的可見版本(有舊的版本)。不加鎖,普通的select語句都是快照讀,如:
select * from core_user where id > 2;
當前讀:讀取的是記錄資料的最新版本,顯式加鎖的都是當前讀
select * from core_user where id > 2 for update;
select * from account where id>2 lock in share mode;
Read View是如何保證可見性判斷的呢?我們先看看Read view 的幾個重要屬性
Read view 匹配條件規則如下:
trx_id < min_limit_id
,表明生成該版本的事務在生成Read View前,已經提交(因為事務ID是遞增的),所以該版本可以被當前事務存取。trx_id>= max_limit_id
,表明生成該版本的事務在生成ReadView後才生成,所以該版本不可以被當前事務存取。min_limit_id =<trx_id< max_limit_id
,需腰分3種情況討論
- (1).如果
m_ids
包含trx_id
,則代表Read View生成時刻,這個事務還未提交,但是如果資料的trx_id
等於creator_trx_id
的話,表明資料是自己生成的,因此是可見的。- (2)如果
m_ids
包含trx_id
,並且trx_id
不等於creator_trx_id
,則Read View生成時,事務未提交,並且不是自己生產的,所以當前事務也是看不見的;- (3).如果
m_ids
不包含trx_id
,則說明你這個事務在Read View生成之前就已經提交了,修改的結果,當前事務是能看見的。
InnoDB 實現MVCC,是通過 Read View+ Undo Log
實現的,Undo Log 儲存了歷史快照,Read View可見性規則幫助判斷當前版本的資料是否可見。
事務A: select * fom core_user where id=1
事務B: update core_user set name =」曹操」
執行流程如下:
最後事務A查詢到的結果是,name=曹操的記錄,我們基於MVCC,來分析一下執行流程:
(1). A開啟事務,首先得到一個事務ID為100
(2).B開啟事務,得到事務ID為101
(3).事務A生成一個Read View,read view對應的值如下
變數 | 值 |
---|---|
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然後回到版本鏈:開始從版本鏈中挑選可見的記錄:
由圖可以看出,最新版本的列name的內容是孫權
,該版本的trx_id
值為100。開始執行read view可見性規則校驗:
min_limit_id(100)=<trx_id(100)<102;
creator_trx_id = trx_id =100;
由此可得,trx_id=100的這個記錄,當前事務是可見的。所以查到是name為孫權
的記錄。
(4). 事務B進行修改操作,把名字改為曹操。把原資料拷貝到undo log,然後對資料進行修改,標記事務ID和上一個資料版本在undo log的地址。
(5) 提交事務
(6) 事務A再次執行查詢操作,新生成一個Read View,Read View對應的值如下
變數 | 值 |
---|---|
m_ids | 100 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然後再次回到版本鏈:從版本鏈中挑選可見的記錄:
從圖可得,最新版本的列name的內容是曹操
,該版本的trx_id
值為101。開始執行Read View可見性規則校驗:
min_limit_id(100)=<trx_id(101)<max_limit_id(102);
但是,trx_id=101,不屬於m_ids集合
因此,trx_id=101
這個記錄,對於當前事務是可見的。所以SQL查詢到的是name為曹操
的記錄。
綜上所述,在讀已提交(RC)隔離級別下,同一個事務裡,兩個相同的查詢,讀取同一條記錄(id=1),卻返回了不同的資料(第一次查出來是孫權,第二次查出來是曹操那條記錄),因此RC隔離級別,存在不可重複讀並行問題。
在RR隔離級別下,是如何解決不可重複讀問題的呢?我們一起再來看下,
還是4.2小節那個流程,還是這個事務A和事務B,如下:
實際上,各種事務隔離級別下的Read view工作方式,是不一樣的,RR可以解決不可重複讀問題,就是跟Read view工作方式有關。
begin | |
---|---|
select * from core_user where id =1 | 生成一個Read View |
/ | / |
/ | / |
select * from core_user where id =1 | 生成一個Read View |
begin | |
---|---|
select * from core_user where id =1 | 生成一個Read View |
/ | |
/ | |
select * from core_user where id =1 | 共用一個Read View副本 |
我們穿越下,回到剛4.2的例子,然後執行第2個查詢的時候:
事務A再次執行查詢操作,複用老的Read View副本,Read View對應的值如下
變數 | 值 |
---|---|
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然後再次回到版本鏈:從版本鏈中挑選可見的記錄:
從圖可得,最新版本的列name的內容是曹操
,該版本的trx_id
值為101。開始執行read view可見性規則校驗:
min_limit_id(100)=<trx_id(101)<max_limit_id(102);
因為m_ids{100,101}包含trx_id(101),
並且creator_trx_id (100) 不等於trx_id(101)
所以,trx_id=101
這個記錄,對於當前事務是不可見的。這時候呢,版本鏈roll_pointer
跳到下一個版本,trx_id=100
這個記錄,再次校驗是否可見:
min_limit_id(100)=<trx_id(100)< max_limit_id(102);
因為m_ids{100,101}包含trx_id(100),
並且creator_trx_id (100) 等於trx_id(100)
所以,trx_id=100
這個記錄,對於當前事務是可見的。即在可重複讀(RR)隔離級別下,複用老的Read View副本,解決了不可重複讀的問題。