引言
以前在深刻了解數據庫理論的時候,瞭解到事務的不一樣隔離級別可能存在的問題。爲了更好的理解因此在MySQL數據庫中測試復現這些問題。關於髒讀和不可重複讀在相應的隔離級別下都很容易的復現了。面試
可是對於幻讀,我發如今可重複讀的隔離級別下沒有出現,當時想到難道是MySQL對幻讀作了什麼處理?sql
測試:數據庫
建立一張測試用的表dept:併發
CREATETABLE`dept`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(20)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=12DEFAULTCHARSET=utf8insertintodept(name)values("後勤部")
根據上面的流程執行,預期來講應該是事務1的第一條select查詢出一條數據,第二個select查詢出兩條數據(包含事務2提交的數據)。性能
可是在實際測試中發現第二條select實際上也只查詢處理一條數據。這是可是根據數據庫理論的可重複讀的實現(排他鎖和共享鎖)這是不該該的狀況。測試
在瞭解實際緣由前咱們先複習下事務的相關理論。atom
數據庫原理理論
事務
事務(Transaction),通常是指要作的或所作的事情。在計算機術語中是指訪問並可能更新數據庫中各類數據項的一個程序執行單元(unit)。事務由事務開始(begin transaction)和事務結束(end transaction)之間執行的全體操做組成。spa
在關係數據庫中,一個事務能夠是一組SQL語句或整個程序。code
爲何要有事務
一個數據庫事務一般包含對數據庫進行讀或寫的一個操做序列。它的存在包含有如下兩個目的:orm
- 爲數據庫操做提供了一個從失敗中恢復到正常狀態的方法,同時提供了數據庫在異常狀態下仍能保持一致性的方法。
- 當多個應用程序在併發訪問數據庫時,能夠在這些應用程序之間提供一個隔離方法,保證彼此的操做互相干擾。
事務特性
事務具備4個特性:原子性、一致性、隔離性、持久性。這四個屬性一般稱爲 ACID 特性。
- 原子性(atomicity):一個事務應該是一個不可分割的工做單位,事務中包括的操做要麼都成功,要麼都不成功。
- 一致性(consistency):事務必須是使數據庫從一個一致性狀態變到另外一個一致性狀態。一致性與原子性是密切相關的。
- 隔離性(isolation):一個事務的執行不能被其餘事務干擾。即一個事務內部的操做及使用的數據在事務未提交前對併發的其餘事務是隔離的,併發執行的各個事務之間不能互相影響。
- 持久性(durability):一個事務一旦成功提交,它對數據庫中數據的改變就應該是永久性的。接下來的其餘操做或故障不該該對其有任何影響。
事務之間的幾個特性並非一組同等的概念:
若是在任什麼時候刻都只有一個事務,那麼其自然是具備隔離性的,這時只要保證原子性就能具備一致性。
若是存在併發的狀況下,就須要保證原子性和隔離性才能保證一致性。
數據庫併發事務中存在的問題
若是不考慮事務的隔離性,會發生如下幾種問題:
髒讀
髒讀是指在一個事務處理過程裏讀取了另外一個未提交的事務中的數據。當一個事務正在屢次修改某個數據,而在這個事務中這屢次的修改都還未提交,這時一個併發的事務來訪問該數據,就會形成兩個事務獲得的數據不一致。
不可重複讀
不可重複讀是指在對於數據庫中的某條數據,一個事務範圍內屢次查詢返回不一樣的數據值(這裏不一樣是指某一條或多條數據的內容先後不一致,但數據條數相同),這是因爲在查詢間隔,該事務須要用到的數據被另外一個事務修改並提交了。
不可重複讀和髒讀的區別是,髒讀是某一事務讀取了另外一個事務未提交的髒數據,而不可重複讀則是讀取了其餘事務提交的數據。須要注意的是在某些狀況下不可重複讀並非問題。
幻讀
幻讀是事務非獨立執行時發生的一種現象。例如事務T1對一個表中全部的行的某個數據項作了從「1」修改成「2」的操做,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值仍是爲「1」而且提交給數據庫。
而操做事務T1的用戶若是再查看剛剛修改的數據,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺同樣,這就是發生了幻讀。
幻讀和不可重複讀都是讀取了另外一條已經提交的事務(這點就髒讀不一樣),所不一樣的是不可重複讀可能發生在update,delete操做中,而幻讀發生在insert操做中。
排他鎖,共享鎖
排它鎖(Exclusive),又稱爲X 鎖,寫鎖。
共享鎖(Shared),又稱爲S 鎖,讀鎖。
讀寫鎖之間有如下的關係:
- 一個事務對數據對象O加了 S 鎖,能夠對 O進行讀取操做,可是不能進行更新操做。加鎖期間其它事務能對O 加 S 鎖,可是不能加 X 鎖。
- 一個事務對數據對象 O 加了 X 鎖,就能夠對 O 進行讀取和更新。加鎖期間其它事務不能對 O 加任何鎖。
即讀寫鎖之間的關係能夠歸納爲:多讀單寫
事務的隔離級別
在事務中存在如下幾種隔離級別:
讀未提交(Read Uncommitted)
解決更新丟失問題。若是一個事務已經開始寫操做,那麼其餘事務則不容許同時進行寫操做,但容許其餘事務讀此行數據。該隔離級別能夠經過「排他寫鎖」實現,即事務須要對某些數據進行修改必須對這些數據加 X 鎖,讀數據不須要加 S 鎖。
讀已提交(Read Committed)
解決了髒讀問題。讀取數據的事務容許其餘事務繼續訪問該行數據,可是未提交的寫事務將會禁止其餘事務訪問該行。這能夠經過「瞬間共享讀鎖」和「排他寫鎖」實現, 即事務須要對某些數據進行修改必須對這些數據加 X 鎖,讀數據時須要加上 S 鎖,當數據讀取完成後馬上釋放 S 鎖,不用等到事務結束。
可重複讀取(Repeatable Read)
禁止不可重複讀取和髒讀取,可是有時可能出現幻讀數據。讀取數據的事務將會禁止寫事務(但容許讀事務),寫事務則禁止任何其餘事務。
Mysql默認使用該隔離級別。這能夠經過「共享讀鎖」和「排他寫鎖」實現,即事務須要對某些數據進行修改必須對這些數據加 X 鎖,讀數據時須要加上 S 鎖,當數據讀取完成並不馬上釋放 S 鎖,而是等到事務結束後再釋放。
串行化(Serializable)
解決了幻讀的問題的。提供嚴格的事務隔離。它要求事務序列化執行,事務只能一個接着一個地執行,不能併發執行。僅僅經過「行級鎖」是沒法實現事務序列化的,必須經過其餘機制保證新插入的數據不會被剛執行查詢操做的事務訪問到。
MySQL中的隔離級別的實現
上面的內容解釋了一些數據庫理論的概念,可是在MySQL、ORACLE這樣的數據庫中,爲了性能的考慮並非徹底按照上面介紹的理論來實現的。
MVCC
多版本併發控制(Multi-Version Concurrency Control, MVCC)是MySQL中基於樂觀鎖理論實現隔離級別的方式,用於實現讀已提交和可重複讀取隔離級別的實現。
實現(隔離級別爲可重複讀)
在說到如何實現前先引入兩個概念:
- 系統版本號:一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
- 事務版本號:事務開始時的系統版本號。
在MySQL中,會在表中每一條數據後面添加兩個字段:
- 建立版本號:建立一行數據時,將當前系統版本號做爲建立版本號賦值
- 刪除版本號:刪除一行數據時,將當前系統版本號做爲刪除版本號賦值
SELECT
select時讀取數據的規則爲:建立版本號<=當前事務版本號,刪除版本號爲空或>當前事務版本號。
建立版本號<=當前事務版本號保證取出的數據不會有後啓動的事務中建立的數據。這也是爲何在開始的示例中咱們不會查出後來添加的數據的緣由
刪除版本號爲空或>當前事務版本號保證了至少在該事務開啓以前數據沒有被刪除,是應該被查出來的數據。
INSERT
insert是將當前的系統版本號賦值給建立版本號字段。
UPDATE
插入一條新記錄,保存當前事務版本號爲行建立版本號,同時保存當前事務版本號到原來刪除的行,實際上這裏的更新是經過delete和insert實現的。
DELETE
刪除時將當前的系統版本號賦值給刪除版本號字段,標識該行數據在那一個事務中會被刪除,即便實際上在位commit時該數據沒有被刪除。根據select的規則後開啓的數據也不會查詢到該數據。
MVCC真的解決了幻讀?
從最開始咱們的測試示例和上面的理論支持來看貌似在MySQL中經過MVCC就解決了幻讀的問題,那既然這樣串行化讀貌似就沒啥意義了,帶着疑問繼續測試。
測試前數據:
根據上面的結果咱們指望的結果是這樣的:
idname1財務部2研發部
可是實際上咱們的通過是:
原本咱們但願獲得的結果只是第一條數據的部門改成財務,可是結果確實兩條數據都被修改了。
這種結果告訴咱們其實在MySQL可重複讀的隔離級別中並非徹底解決了幻讀的問題,而是解決了讀數據狀況下的幻讀問題。而對於修改的操做依舊存在幻讀問題,就是說MVCC對於幻讀的解決時不完全的。
快照讀和當前讀
出現了上面的狀況咱們須要知道爲何會出現這種狀況。在查閱了一些資料後發如今RR級別中,經過MVCC機制,雖然讓數據變得可重複讀,但咱們讀到的數據多是歷史數據,不是數據庫最新的數據。
這種讀取歷史數據的方式,咱們叫它快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫當前讀 (current read)。
select 快照讀
當執行select操做是innodb默認會執行快照讀,會記錄下此次select後的結果,以後select 的時候就會返回此次快照的數據,即便其餘事務提交了不會影響當前select的數據,這就實現了可重複讀了。
快照的生成當在第一次執行select的時候,也就是說假設當A開啓了事務,而後沒有執行任何操做,這時候B insert了一條數據而後commit,這時候A執行 select,那麼返回的數據中就會有B添加的那條數據。
以後不管再有其餘事務commit都沒有關係,由於快照已經生成了,後面的select都是根據快照來的。
當前讀
對於會對數據修改的操做(update、insert、delete)都是採用當前讀的模式。在執行這幾個操做時會讀取最新的記錄,即便是別的事務提交的數據也能夠查詢到。
假設要update一條記錄,可是在另外一個事務中已經delete掉這條數據而且commit了,若是update就會產生衝突,因此在update的時候須要知道最新的數據。也正是由於這樣因此才致使上面咱們測試的那種狀況。
select的當前讀須要手動的加鎖:
select*fromtablewhere?lockinsharemode;select*fromtablewhere?forupdate;
有個問題說明下
在測試過程當中最開始我覺得使用begin語句就是開始一個事務了,因此在上面第二次測試中由於先開始的事務1,結果在事務1中卻查到了事務2新增的數據,當時認爲這和前面MVCC中的select的規則不一致了,因此作了以下測試:
SELECT*FROMinformation_schema.INNODB_TRX//用於查詢當前正在執行中的事務
能夠看到若是隻是執行begin語句實際上並無開啓一個事務。
下面在begin後添加一條select語句:
因此要明白其實是對數據進行了增刪改查等操做後纔開啓了一個事務。
如何解決幻讀
很明顯可重複讀的隔離級別沒有辦法完全的解決幻讀的問題,若是咱們的項目中須要解決幻讀的話也有兩個辦法:
- 使用串行化讀的隔離級別
- MVCC+next-key locks:next-key locks由record locks(索引加鎖) 和 gap locks(間隙鎖,每次鎖住的不光是須要使用的數據,還會鎖住這些數據附近的數據)
實際上不少的項目中是不會使用到上面的兩種方法的,串行化讀的性能太差,並且其實幻讀不少時候是咱們徹底能夠接受的。