事物 1 | 事物 2 |
---|---|
begin | begin |
select * from dept | |
- | insert into dept(name) values("研發部") |
- | commit |
select * from dept | |
commit |
根據上面的流程執行,預期來說應該是事物1的第一條select查詢出一條數據,第二個select查詢出兩條數據(包含事物2提交的數據)。
但是在實際測試中發現第二條select實際上也只查詢處理一條數據。這是但是根據數據庫理論的可重復讀的實現(排他鎖和共享鎖)這是不應該的情況。
在了解實際原因前我們先復習下事物的相關理論。
數據庫原理理論
事物
事務(Transaction),一般是指要做的或所做的事情。在計算機術語中是指訪問并可能更新數據庫中各種數據項的一個程序執行單元(unit)。事務由事務開始(begin transaction)和事務結束(end transaction)之間執行的全體操作組成。在關系數據庫中,一個事務可以是一組SQL語句或整個程序。
為什么要有事物
一個數據庫事務通常包含對數據庫進行讀或寫的一個操作序列。它的存在包含有以下兩個目的:
事物特性
事務具有4個特性:原子性、一致性、隔離性、持久性。這四個屬性通常稱為 ACID 特性。
事物之間的幾個特性并不是一組同等的概念:
如果在任何時刻都只有一個事物,那么其天然是具有隔離性的,這時只要保證原子性就能具有一致性。
如果存在并發的情況下,就需要保證原子性和隔離性才能保證一致性。
數據庫并發事物中存在的問題
如果不考慮事務的隔離性,會發生以下幾種問題:
排他鎖,共享鎖
排它鎖(Exclusive),又稱為X 鎖,寫鎖。
共享鎖(Shared),又稱為S 鎖,讀鎖。
讀寫鎖之間有以下的關系:
即讀寫鎖之間的關系可以概括為:多讀單寫
事物的隔離級別
在事物中存在以下幾種隔離級別:
MySQL中的隔離級別的實現
上面的內容解釋了一些數據庫理論的概念,但是在MySQL、ORACLE這樣的數據庫中,為了性能的考慮并不是完全按照上面介紹的理論來實現的。
MVCC
多版本并發控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于樂觀鎖理論實現隔離級別的方式,用于實現讀已提交和可重復讀取隔離級別的實現。
實現(隔離級別為可重復讀)
在說到如何實現前先引入兩個概念:
系統版本號:一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
事務版本號:事務開始時的系統版本號。
在MySQL中,會在表中每一條數據后面添加兩個字段:
創建版本號:創建一行數據時,將當前系統版本號作為創建版本號賦值
刪除版本號:刪除一行數據時,將當前系統版本號作為刪除版本號賦值
SELECT
select時讀取數據的規則為:創建版本號=當前事務版本號,刪除版本號為空或>當前事務版本號。
創建版本號=當前事務版本號保證取出的數據不會有后啟動的事物中創建的數據。這也是為什么在開始的示例中我們不會查出后來添加的數據的原因
刪除版本號為空或>當前事務版本號保證了至少在該事物開啟之前數據沒有被刪除,是應該被查出來的數據。
INSERT
insert時將當前的系統版本號賦值給創建版本號字段。
UPDATE
插入一條新紀錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行,實際上這里的更新是通過delete和insert實現的。
DELETE
刪除時將當前的系統版本號賦值給刪除版本號字段,標識該行數據在那一個事物中會被刪除,即使實際上在位commit時該數據沒有被刪除。根據select的規則后開啟懂數據也不會查詢到該數據。
MVCC真的解決了幻讀?
從最開始我們的測試示例和上面的理論支持來看貌似在MySQL中通過MVCC就解決了幻讀的問題,那既然這樣串行化讀貌似就沒啥意義了,帶著疑問繼續測試。
測試前數據:
事物 1 | 事物 2 |
---|---|
begin | begin |
select * from dept | |
- | insert into dept(name) values("研發部") |
- | commit |
update dept set name="財務部"(工作中如果不想被辭退一定要寫where條件) | |
commit |
根據上面的結果我們期望的結果是這樣的:
id name
1 財務部
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 * from table where ? lock in share mode; select * from table where ? for update;
有個問題說明下
在測試過程中最開始我以為使用begin語句就是開始一個事物了,所以在上面第二次測試中因為先開始的事物1,結果在事物1中卻查到了事物2新增的數據,當時認為這和前面MVCC中的select的規則不一致了,所以做了如下測試:
SELECT * FROM information_schema.INNODB_TRX //用于查詢當前正在執行中的事物
可以看到如果只是執行begin語句實際上并沒有開啟一個事物。
下面在begin后添加一條select語句:
所以要明白實際上是對數據進行了增刪改查等操作后才開啟了一個事物。
如何解決幻讀
很明顯可重復讀的隔離級別沒有辦法徹底的解決幻讀的問題,如果我們的項目中需要解決幻讀的話也有兩個辦法:
實際上很多的項目中是不會使用到上面的兩種方法的,串行化讀的性能太差,而且其實幻讀很多時候是我們完全可以接受的。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對腳本之家的支持。
上一篇:MySQL不同表之前的字段復制