搞懂MySQL對事務的破壞與補救措施,掃清對事務的誤區
數據庫事務是對數據庫中數據一系列操做的工做單元,這個單元應該保證四大特性:A(原子性)、C(一致性)、I(隔離性)、D(持久性)。這個工做單元就是咱們平時說的事務,而其四大特性是爲了解決現實問題而提出的規約,不一樣的數據庫實現事務時都應該依照該規約。由於後續內容都是對這規約的實現,因此須要先明確這四個特性:mysql
- 原子性(Atomicity):事務中的一系列操做是一個不可分割的最小單元,對數據操做表現爲:這系列操做要麼所有成功,要麼所有失敗。
- 一致性(Consistency):事務的的結果應該是符合事務中數據操做的預測結果的,對數據庫表現爲:這系列操做的最終結果就是想要的結果。
- 隔離性(Isolation):存在多個事務時,各個事務之間的系列操做是互不影響的,相互隔離的。
- 持久性(Durability):事務一旦完成,事務的結果應該是持久保存下來的,不會由於任何緣由致使該結果丟失。
關係型數據庫是依照該規約來實現事務的,其中原子性、一致性、持久性通常是經過文件記錄方式來實現,而隔離性則是經過鎖機制實現的。sql
好了,到這裏就基本瞭解事務了,先低頭沉思一個問題:事務帶來了髒讀、幻讀、不可重複讀的問題?而後再繼續往下看,學技術不要隨大流,要有本身的看法和思考。數據庫
這篇文章主要說隔離性,在讀一遍隔離性特色:存在多個事務時,各個事務之間的系列操做是互不影響的,相互隔離的。實際狀況中隔離性是經過鎖來實現的。若是將數據庫比較爲圖書館,館中每一個書櫃上都是一張表,爲了實現隔離特性,能夠在圖書館大門上加一把鎖,鑰匙只有一把,這樣同時圖書館中只會有一我的,各個期間就實現了隔離特性(對於其餘特性的文件記錄能夠理解爲圖書館監控把每一個人的行爲都記錄到文檔中)。這是對事務的隔離特性的完美實現,也是事務隔離特性的要求,這種實現就是串行化。讀到這裏再思考下那個問題。併發
一個圖書館同時只提供一我的進入,太浪費資源了。是啊,太浪費資源了,可是若是提供併發能力(破壞隔離性)每每會破壞數據的一致性,所以須要在併發量和一致性之間找到一個平衡。最後大佬們根據對數據一致性破壞的程度將事務的隔離性區分了4個級別,一致性破壞形成的影響區分爲:髒讀、幻讀、不可重複讀,這就是ANSI SQL-92標準。高併發
到這裏,就能夠回覆上面那個問題了:髒讀、幻讀、不可重複讀是爲了併發能力而破壞了事務的隔離性而產生的,而不是事務形成的,是一種異常現象。優化
併發能力的提升
隔離性破壞
破壞隔離性,意味着多個事務能夠相互影響,而事務自己的原子性還在。那麼影響程度能夠區分爲:1.讀取到了其餘事務成功的結果(已提交)2.讀取到了其餘事務失敗的結果(未提交)。這樣以來隔離特性暫時能夠區分爲兩種:讀已提交、讀未提交,加上最基本的串行化,目前有三種隔離級別。url
一致性破壞
上面說破壞隔離性每每會致使破壞一致性,先說下不一樣破壞程度形成的影響程度:髒讀、幻讀、不可重複讀spa
髒讀
在一個事務中的屢次讀取中,該事務某次讀取到了其餘事務中未提交的數據,形成了該次讀取的中出現了任何和上次讀取的結果不一致的現象。.net
白話解釋:髒讀是讀取到了此刻不該該存在的髒數據(此時此刻該數據還在醞釀中,尚未呱呱落地)。指針
不可重複讀
在一個事務中的屢次讀取中,該事務某次讀取讀取到了其餘事務中update或者delete提交的數據,形成了該次讀取的結果與上次讀取的結果中同行的數據某列值不相同或者數據行變少的現象。
白話解釋:不可重複讀是讀取到了本應該已存在卻被修改的數據(數據篡改成不可重複讀)
幻讀
在一個事務中的屢次讀取中,該事務某次讀取讀取到了其餘事務中insert提交的數據,形成了該次讀取的結果與上次讀取的結果中行數變多的現象
白話解釋:幻讀是讀取到了除了本應該存在的數據還有憑空造出來的數據(無中生有爲幻讀)
讀未提交會產生一個數據不一致現象就是不可重複讀,而現實意義中不可重複讀基本等價於數據沒有價值(你們去圖書館去查看參考資料,結果每一個人看到的都不同),基於現實意義,在讀已提交和串行化之間增長了一個可重複讀的隔離級別。至此隔離性破壞程度區分爲四種,基本知足數據的現實需求了
隔離性的破壞程度對一致性的影響程度關係
事務隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
ISOLATION_READ_UNCOMMITTED(讀未提交) | 可能 | 可能 | 可能 |
ISOLATION_READ_COMMITTED(讀已提交) | 不可能 | 可能 | 可能 |
ISOLATION_REPEATABLE_READ(可重複讀) | 不可能 | 不可能 | 可能 |
ISOLATION_SERIALIZABLE(串行化) | 不可能 | 不可能 | 不可能 |
這裏注意可重複讀不能解決幻讀問題
隔離性破壞方案MVCC
隔離性破壞主要解決的是併發能力,隔離性體現爲多個事務互不影響,一旦破壞則存在如下狀況
- A讀-B讀:無影響
- A讀-B寫:A事務可能讀取到B事務修改、新增、刪除的數據,形成數據不一致
- A寫-B寫:同行數據同一時刻只容許A事務或者B事務一個寫,目前只能經過基於鎖的併發控制技術(LBCC)來解決。
以上狀況中,只有A讀-B寫這種狀況能被無鎖化來提升併發能力。優化方案就是多版本併發控制(MVCC)技術
MVCC
MVCC基本意思是不一樣事務對數據庫的修改操做,不會覆蓋掉之前的舊數據(不會篡改數據了,也就不會有不可重複讀),而是產生一個新數據,實現舊版本數據和新版本數據共同存在,事務讀取時只讀取與本身相關版本的數據行。這樣多個版本存在的數據行就不會受到其餘事務的影響,從而不須要鎖就能產生隔離性。想一想圖書館中的每一個人都拿的是印刷版本,而不是孤本,這樣你們讀書就互不影響了。因爲版本數據的隔離性,故能夠處理不可重複讀問題。
注意:MVCC針對支持事務隔離性破壞而提出的解決方案,也就是說MVCC只在支持事務的存儲引擎,如InnoDB才存在,
並且MVCC只適用於RC和RR級別(其餘級別沒有意義)。
不一樣數據對MVCC的實現不一樣。MySQL中主要是對數據行添加2個隱藏列來體現多版本。這兩個列分別爲事務ID(db_trx_id)、回滾指針(db_roll_pointer),也就是一個表中行數據存儲格式以下,
db_row_id是保證表聚簇索引時才存在的(主鍵不存在且非null惟一索引也不存在時)
db_trx_id是存儲數據行所屬的事務ID(事務ID是一個自增的ID)
db_roll_pointer是執行undo日誌中上個版本的數據行的指針
最終造成一個版本鏈,因爲undo日誌記錄的數據也能夠看作一個版本,這裏簡化後的版本鏈以下
ReadView
版本鏈中數據行這麼多,一個事務怎麼知道本身能看到哪些數據行呢?MySQL提出了ReadView概念來解決這個問題。
ReadView: 可讀視圖?一致性視圖,ReadView在RR級別事務中第一個select操做就開始構建,RC級別每次select都會從新構建。ReadView在5.7版本中構造器以下
ReadView::ReadView() : m_low_limit_id(), m_up_limit_id(), m_creator_trx_id(), m_ids(), m_low_limit_no() { ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list))); }
看不懂不要緊,下面介紹下主要的構造參數
- m_ids:存活的事務ID列表(ReadView建立時的存活事務ID列表,這是一個逆序列表,最底層是最近開始的事務,ID最大)
- low_limit_id: 當前事務可見低水位事務ID(實際上是最大事務ID,要分配給下一個事務的ID)
- up_limit_id:當前事務可見高水位事務ID(實際上是最小事務ID,若是m_ids爲空,則最大與最小一致)
版本數據行的可見性與事務ID密切相關。所以須要瞭解到事務具備時間維度後具備的如下特色
- 事務建立有前後,事務ID是自增的,所以事務ID總體有序;
- 事務結束有前後,致使提交時事務的ID是無序的;
版本行的可見性區分邊界爲ReadView建立時間點,根據其餘事務結束時間點與ReadView時間點前後順序區分以下
所以按照ReadView時間點就能夠給事務劃分種類
- 將在ReadView建立點以前已提交的劃分爲一種
- 將在ReadView建立點以後還未已提交的(以前已開啓)劃分爲一種,將這類事務做爲存活事務做爲m_ids,排除自身事務ID後逆序排序,其中事務ID最小的做爲up_limit_id,若是m_ids爲空,up_limit_id=low_limit_id。
- 將在ReadView建立點以後新建立的事務劃分爲一種,將這類事務中ID最小的做爲low_limit_id
獲取版本鏈中可見數據行方式
全部數據行中其事務ID若是在存活的事務ID列表中則表示不可見,逆序查詢版本鏈,直至觸發終止條件,版本鏈逆序結束。(刪除的數據行對任何事務都是不可見的)。
終止條件是根據以上參數條件獲得的,事務結束時的事務ID只與事務建立時間正相關,而與ReadView的落點沒有太大關係,優化以後存在如下關係
- 知足小於up_limit_id的便可終止(建立點以前已提交數據行均可見)
- 知足大於等於low_limit_id的便可終止(建立點以後數據行都不可見)
- 在m_ids中的數據行都不可見
ReadView的最終效果:建立ReadView時,已經提交的最新版本數據行就是可見數據行,最終查詢到的結果也是混這個數據行。
查詢結果
RR級別時:在首次select操做時,只有前面已提交的事務結果纔會被當前事務查詢到。
RC級別時:在每次select操做時,只要是本次select以前別提交的事務結果纔會被當前事務查詢到。
解決的問題
RR級別:解決髒讀、不可重複讀
RC級別:解決髒讀
特殊:若是讀取的數據都是版本數據,則能解決幻讀(版本的完美隔離性),也就是說能解決快照讀的幻讀。若是當前讀則不能解決(當前讀有可能產生新的快照數據)。
快照讀與當前讀
快照讀
前面提到Read View,它讀取的數據都是版本數據,這些版本數據就是快照數據。快照數據在建立Read View時建立。普通(無鎖的)的select都是快照讀。
當前讀
前面說MVCC中舊版本數據和新版本數據共存,若是其餘事務也想建立本身的舊版本數據,那麼舊版本數據從哪裏來?是從已提交數據中最新版本中拿來的,這種讀取就是當前讀。當前讀取就是讀取已提交的持久數據。帶鎖的select(select xxx from table for update 、select xx from table lock in share mode)和insert、update、delete(都要先讀取到才能繼續操做)都是當前讀。
被當前讀讀取到的數據會被加鎖:lock in share mode使用S鎖(共享鎖),其餘都是X鎖(排他鎖)。
這裏說明下不能解決幻讀的緣由:當前讀能讀取到後續且已提交其餘事務中的執行結果。RR舉例
例1:影響到其餘事務的結果
mysql> SELECT * from read_view where id < 4; +----+------+ | id | name | +----+------+ | 1 | 張三 | | 3 | 王五 | +----+------+ 2 rows in set (0.03 sec)
開啓事務A,select開啓快照
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * from read_view where id < 4; +----+------+ | id | name | +----+------+ | 1 | 張三 | | 3 | 王五 | +----+------+ 2 rows in set (0.03 sec)
事務B執行且提交
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update read_view set name='王六' where id <4; Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) mysql> select * from read_view where id < 4; +----+------+ | id | name | +----+------+ | 1 | 王六 | | 3 | 王六 | +----+------+ 2 rows in set (0.03 sec)
事務A繼續執行,先快照讀印證可重複讀,在當前讀
mysql> select * from read_view where id <4; +----+------+ | id | name | +----+------+ | 1 | 張三 | | 3 | 王五 | +----+------+ 2 rows in set (0.07 sec) mysql> update read_view set name='王麻子' where id < 4; Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0 mysql> select * from read_view where id <4; +----+--------+ | id | name | +----+--------+ | 1 | 王麻子 | | 3 | 王麻子 | +----+--------+ 2 rows in set (0.08 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) mysql> select * from read_view where id <4; +----+--------+ | id | name | +----+--------+ | 1 | 王麻子 | | 3 | 王麻子 | +----+--------+ 2 rows in set (0.07 sec)
事務B在快照讀
mysql> select * from read_view where id < 4; +----+--------+ | id | name | +----+--------+ | 1 | 王麻子 | | 3 | 王麻子 | +----+--------+ 2 rows in set (0.04 sec)
代碼雖然有點多餘,可是都在作驗證,已經能看到後續事務B的結果被事務A修改了,也就是說事務A影響到了後續已提交事務的結果。
例2:幻讀示例,基礎數據不動,操做流程不動,將例1中事務B的update操做改成insert操做
事務B操做
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert read_view(id,name) values(2,'李四'); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) mysql> select * from read_view where id < 4; +----+------+ | id | name | +----+------+ | 1 | 張三 | | 2 | 李四 | | 3 | 王五 | +----+------+ 3 rows in set (0.03 sec)
事務A繼續操做
mysql> select * from read_view where id <4; +----+--------+ | id | name | +----+--------+ | 1 | 張三 | | 3 | 王麻子 | +----+--------+ 2 rows in set (0.03 sec) mysql> update read_view set name='王麻子' where id<4; Query OK, 2 rows affected (0.00 sec) Rows matched: 3 Changed: 2 Warnings: 0 mysql> select * from read_view where id <4; +----+--------+ | id | name | +----+--------+ | 1 | 王麻子 | | 2 | 王麻子 | | 3 | 王麻子 | +----+--------+ 3 rows in set (0.03 sec)
事務A還沒提交就已經發現問題了,憑空多出來一行,無中生有即爲幻讀。
出現幻讀的緣由是:update的當前讀能讀取到其餘事務的執行結果,順便給當前事務新增了本來不屬於當前事務的快照數據(已有的還在,沒有的建立快照)。
解決幻讀的方式就是:建立ReadView的select使用當前讀,給相關數據行加鎖,防止其餘事務修改。這裏使用間隙鎖就能夠了,由於已存在的快照數據不會被其餘事務影響,只有不存在的快照數據纔會出現新增快照數據的狀況。間隙鎖就能防止新增快照數據。若是不想使用鎖,就要保證select以後出現的任何當前讀操做只涉及到select時的結果,不能超出其範圍便可,這樣能一樣防止新增快照數據。
LBCC
前面提到提升併發能力一個手段就是LBCC(基於鎖的併發控制),這裏就一塊說下鎖機制,也能瞭解間隙鎖爲何能防止幻讀。針對多事務之間涉及到寫-寫操做都會用的鎖,鎖分爲表鎖和行鎖
表鎖
表鎖是鎖的數據庫的某一整張表,區分爲讀鎖和寫鎖,用法以下
LOCK tables lock_demo READ; -- 表加鎖,讀鎖 UNLOCK tables ; -- 表解鎖 LOCK tables lock_demo WRITE; -- 表加鎖,寫鎖:其餘事務不能讀、不能寫 UNLOCK tables ; -- 表解鎖
行鎖
行鎖是鎖的數據庫表中的某一行數據,區分爲共享鎖(S鎖)和排他鎖(X鎖)。行鎖對應的索引數據行不存在則會升級爲表鎖
共享鎖
共享鎖也稱爲讀鎖、S鎖,鎖定的數據行能夠被多個事務同時讀取,不能修改
上鎖
select xxx from table where xxx lock in share mode;
解鎖
事務結束(commit、rollback)、進程結束
排他鎖
排他鎖也稱爲寫鎖、X鎖,鎖定的數據行不能被其餘事務讀取,固然也不會被修改了。排他鎖包含記錄鎖、間隙鎖、臨鍵鎖
上鎖
select xxx from table where xxx for update; update table set xxx=xxx where xxx delete from table where xxx insert table (xxx) values (xxx)
解鎖
事務結束(commit、rollback)、進程結束
對比
表級鎖: 開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的機率最高,併發度最低
行級鎖: 開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的機率最低,併發度也最高
MyISAM:只支持表鎖
InnoDB:支持表鎖和行鎖
鎖細節
存在表lock_demo,其中id爲主鍵,age爲普通索引,數據以下
id | age |
1 | 15 |
2 | 56 |
3 | 67 |
記錄鎖:對聚簇索引值進行精確匹配鎖定,鎖定的整行數據
示例
BEGIN ; select * from lock_demo where id=1 for update;
記錄鎖
BEGIN; UPDATE lock_demo set age=16 WHERE age =15; -- 15 對應的id=1,在聚簇引樹中已產生記錄鎖,所以阻塞
間隙鎖:會鎖定匹配範圍內索引列對應值的內部間隙和首尾間隙
- 對非聚簇索引樹中的索引列的進行加鎖語句,無論數據是否存在,都會產生間隙鎖
示例:
BEGIN; select * from lock_demo where age=16 for update;
間隙鎖
BEGIN; INSERT into lock_demo VALUES(null,10); -- 10 在(-∞,15)區間內,外部非相鄰間隙,所以能夠執行 BEGIN; INSERT into lock_demo VALUES(null,55); -- 55 在(15,56)區間內,所以阻塞 -- 內部間隙 BEGIN; INSERT into lock_demo VALUES(null,77); -- 77 在(77,+∞)區間內,外部非相鄰間隙,所以能夠執行
臨鍵鎖:會鎖定匹配範圍內索引列對應值的內部全部空間和首尾間隙(間隙鎖+記錄鎖)
- 間隙鎖的範圍內若是存在記錄,則升級爲臨鍵鎖
- 對聚簇索引樹中的索引列進行不存在的單條記錄加鎖語句,會產生間隙鎖和記錄鎖
- 對聚簇索引樹中的索引列進行範圍加鎖語句,無論數據是否存在,會產生間隙鎖和記錄鎖
示例
BEGIN; select * from lock_demo where age BETWEEN 1 and 57 for update;
- 間隙鎖鎖定的是範圍值且不包含索引值的先後2兩個間隙,示例中索引值=15,則間隙鎖鎖定的是(-∞,15)和(15,56)和(56,67)三個間隙
間隙鎖:針對的是間隙,對間隙操做只能是插入語句
BEGIN; INSERT into lock_demo VALUES(null,10); -- 10 在(-∞,15)區間內,所以阻塞 -- 前間隙 BEGIN; INSERT into lock_demo VALUES(null,55); -- 55 在(15,56)區間內,所以阻塞 -- 內部間隙 BEGIN; INSERT into lock_demo VALUES(null,66); -- 66 在(56,67)區間內,所以阻塞 -- 後間隙 BEGIN; INSERT into lock_demo VALUES(null,77); -- 77 在(77,+∞)區間內,所以能夠執行
記錄鎖:針對的是行記錄,對記錄操做只能是更新和刪除
BEGIN; UPDATE lock_demo set age=16 WHERE age =15; -- 15 對應的id=1,在聚簇引樹中已產生記錄鎖,所以阻塞 BEGIN; DELETE from lock_demo where id=1; -- 對應的id=1,在聚簇引樹中已產生記錄鎖,所以阻塞
索引區間劃分以下
表鎖和行數其實也能夠共存的,例如事務A使用共享鎖住了整張表,而事務B也使用共享鎖住了整張表,根據鎖的機制,那麼這兩個事務難道只能串行化才能處理嗎?這兩個事務明明不衝突,爲何不能同時存在?因而意向鎖便出現了
若是事務想要給表中幾行數據加上行級共享鎖,那麼須要先在表級別加上意向共享鎖(IS);
若是事務想要給表中幾行數據加上行級排他鎖,那麼須要先在表級別加上意向排他鎖(IX);
經過意向鎖檢測能夠優化加鎖操做
X | IX | S | IS | |
X | 衝突 | 衝突 | 衝突 | 衝突 |
IX | 衝突 | 兼容 | 衝突 | 兼容 |
S | 衝突 | 衝突 | 兼容 | 兼容 |
IS | 衝突 | 兼容 | 兼容 | 兼容 |
意向間隙鎖
間隙空間很大,若是多個事務使用的間隙互不影響,也應該能同時進行。因而經過意向間隙鎖來預約間隙位置,經過意向間隙鎖就能夠檢查多個間隙插入是否衝突,若是不衝突就能夠併發執行。
自增鎖
專用於自增列的鎖,這是一個表鎖,一張表的自增ID是串行化的。固然這種自增鎖行爲是能夠被優化的
配置參數:innodb_autoinc_lock_mode
值0 ,1,2
- 0:表鎖,一直持有自增鎖,直到執行完插入語SQL後才釋放自增鎖。效率最低,頗有可能阻塞
- 1:MySQL5.7默認值,表鎖,對於已知插入行數的簡單插入語句,批量鎖獲取足夠的自增ID後,釋放自增鎖給其餘事務使用,而後執行插入SQL。對於未知插入行數的批量插入語句,效果等同於0。具備伸縮性,效率相對居中,但也會出現阻塞
- 2:MySQL8.0默認值,獲取一批自增ID(部分)後,把自增鎖釋放(交給其餘事務去執行插入),後續再獲取自增鎖來獲取一批自增ID(部分),再釋放,這樣交錯獲取ID,雖然最終ID是自增的,可是頗有可能不是連續的。固然這種效率也是最高的。
簡單插入:在預處理SQL時就能知道插入的行數。例如 insert into xxx values()、insert into xxx values()()、replace into xxx values()、replace into xxx values()()、
批量插入:在預處理SQL時不能知道插入的行數,只有真正執行時才知道。例如: insert xxx select、replace xxx select 、load data