臟讀、幻讀,要想搞懂不容易!
本文轉(zhuǎn)載自微信公眾號「小姐姐味道」,作者小姐姐養(yǎng)的狗02號。轉(zhuǎn)載本文請聯(lián)系小姐姐味道公眾號。
臟讀、幻讀、不可重復(fù)讀、當(dāng)前讀、快照讀,這些名詞經(jīng)常搞的讓人頭暈。因為一般人大腦的主線就是單線程的,并不能一次性處理多個事務(wù)。
要想記憶深刻,我們得借助幾個實例。讀完本文,你一定會豁然開朗,忍不住三連走起。
但在這之前,我們需要看一下當(dāng)前的數(shù)據(jù)庫隔離級別,到底是什么。比如MySQL。
- select @@tx_isolation;
MySQL就包含4種隔離級別,隔離的當(dāng)然是數(shù)據(jù)。要修改隔離級別的話,可以使用下面的SQL語句。
- set session transaction isolation level read uncommitted;
- set session transaction isolation level read committed;
- set session transaction isolation level repeatable read;
- set session transaction isolation level serializable;
ok,我們創(chuàng)建一張小小的測試表,來看一下并發(fā)環(huán)境下的魔幻效果。
- CREATE TABLE `xjjdog_tx` (
- `id` INT(11) NOT NULL,
- `name` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci',
- `money` BIGINT(20) NOT NULL DEFAULT '0',
- PRIMARY KEY (`id`) USING BTREE
- )
- COLLATE='utf8_general_ci'
- ENGINE=InnoDB
- ;
- INSERT INTO `xjjdog_tx` (`id`, `name`, `money`) VALUES (2, 'xjjdog1', 100);
- INSERT INTO `xjjdog_tx` (`id`, `name`, `money`) VALUES (1, 'xjjdog0', 100);
1. 臟讀
臟讀,意思就是讀出了臟數(shù)據(jù)。啥叫臟數(shù)據(jù)?就是另外一個事務(wù)還沒有提交的數(shù)據(jù)。在read uncommitted隔離級別下,就會出現(xiàn)臟讀。比如下面這個時序
- 事務(wù) A:set session transaction isolation level read uncommitted;
- 事務(wù) B:set session transaction isolation level read uncommitted;
- 事務(wù) A:START TRANSACTION ;
- 事務(wù) B:START TRANSACTION ;
- 事務(wù) A:UPDATE xjjdog_tx SET money=money+100 WHERE NAME='xjjdog0';
- 事務(wù) B:UPDATE xjjdog_tx SET money=money+100 WHERE NAME='xjjdog0';
- 事務(wù) A:ROLLBACK ;
- 事務(wù) B:COMMIT ;
- 事務(wù) B:SELECT * FROM xjjdog_tx ;
在這個場景下,money的原始值為100,分別在兩個session中進行了加100的操作,然后回滾了其中的一個session事務(wù)。結(jié)果,經(jīng)過查詢,發(fā)現(xiàn)money的值保持100不變。也就是其中一次加100的操作被覆蓋掉了。
所以臟讀發(fā)生有幾個條件。
- 高并發(fā)場景,在一個事務(wù)A開始之后還沒結(jié)束之前,有另外一個事務(wù)參與了事務(wù)A所涉及的數(shù)據(jù)行讀寫
- 事務(wù)隔離級別處于最低的讀未提交
- 在你使用到這些數(shù)據(jù)之后,事務(wù)A回滾,造成你之前拿到的數(shù)據(jù)已經(jīng)不再存在
解決方式,只需要設(shè)置成隔離級別比read uncommitted高即可。
2. 不可重復(fù)讀
把隔離級別設(shè)置成read committed即可避免臟讀,這其實非常好理解。臟讀產(chǎn)生的根本原因就是在事務(wù)的執(zhí)行期間有別的操作亂入,這個隔離級別要求事務(wù)A提交之后,修改后的值,才能被事務(wù)B讀到,所以臟讀是不可能會發(fā)生的,從根本上杜絕了。
但read commited會發(fā)生不可重復(fù)讀的情況。
顧名思義,就是在一個事務(wù)周期內(nèi),對于一個值的讀取,產(chǎn)生了兩個結(jié)果。
不可重復(fù)讀,證明了世界并不是總圍繞著你轉(zhuǎn)的。在你的事務(wù)執(zhí)行期間,會有無數(shù)的其他事務(wù)執(zhí)行,如果你的事務(wù)持續(xù)時間超過了這些事務(wù),那么你就可能讀到兩個或者更多的值。
讓我來給你講一個故事。
從前,有一顆桃樹,長了12棵桃子。有一只猴子,叫做xjjdog,它想吃上面的桃子,但桃子還不熟。
第二天去看的時候,它發(fā)現(xiàn)桃子少了一個,變成了11個,經(jīng)過仔細打聽,原來是被猴子A搶先吃掉一個。
第二天去看的時候,桃子又少了一個,變成了10個,原來是被饞嘴的猴子B吃掉一個。
如此這般,桃子一天天少了下去,只剩下最后的2個了,但桃子還是沒熟。
再不摘桃子就沒了,xjjdog摘下了最后的2個桃子,正打算大快朵頤,結(jié)果跳出一只猴子X,說我盯著這些桃子已經(jīng)1年了...
在這故事中,猴子A、B的事務(wù)持續(xù)周期是1天;xjjdog的事務(wù)持續(xù)周期是直到桃子成熟;猴子X的持續(xù)周期更長,可能是一年。它們每天看到的桃子,并不總是12個。今天的桃子,可能被其他的猴子(事務(wù))給吃掉了,造成了觀測的結(jié)果是不一樣的,這就是不可重復(fù)讀的概念。
有時候,即使讀到的值是一樣的,也不能證明沒問題。比如有財務(wù)挪用了2億去炒股,然后在月底把2億還了回來,雖然最終的金額都是一致的,但由于你的對賬周期長,就發(fā)現(xiàn)不了這種差異。
如何解決不可重復(fù)讀呢?先要看一下不可重復(fù)讀是不是問題。
有的系統(tǒng),要求的就是這樣的邏輯,每次在事務(wù)中讀取到不一樣的值,它是可以忍受的。但如果你想要在桃子成熟之前,桃子的數(shù)量都在你的掌控之中,那不可重復(fù)讀就是一種問題。
一種非常好的方式,就是xjjdog一直站在桃樹地下。當(dāng)有別的猴子想要摘桃,就把它趕走。這種方式可行,但在數(shù)據(jù)庫中非常低效,這是serializable級別的做法。
MySQL有一個默認的事務(wù)隔離級別,叫做repeatable read,使用了MVCC的方式(innodb),要更輕量級一些。
3. 可重復(fù)讀
這就是MVCC(Multi-Version Concurrency Control)的功勞了,它有三個特點。
每行數(shù)據(jù)都存在一個版本,每次數(shù)據(jù)更新時都更新該版本
修改時,拷貝一份,當(dāng)前版本隨意修改,事務(wù)之間無干擾
保存時比較版本號,如果成功commit覆蓋原記錄,失敗則rollback
MVCC在InnoDB中的實現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞并發(fā)讀。它的實現(xiàn)關(guān)鍵也有三項技術(shù):
- 3個隱式字段:DB_TRX_ID,最近修改它的事務(wù)ID;DB_ROLL_PTR,回滾指針,指向上一個版本;DB_ROW_ID,隱藏主鍵
- undo日志:的對同一記錄的修改,會生成針對此記錄的版本變更鏈表
- read view:快照讀操作的時候,產(chǎn)生的讀視圖。除了使用上面的額外信息,它也會維護一個活躍的事務(wù)ID集合
一切的關(guān)鍵,就在于快照這兩個字上面。
比如事務(wù)A對某個記錄進行了快照讀,那么在快照讀的這一刻,就生成了一個Read View。在這一刻,事務(wù)B和C,還沒有commit,事務(wù)D和E,在建立ReadView那一刻之前,commit完成,那么這個Read View,就不能夠讀到B和C的修改。
但可惜的是,可重復(fù)讀,只能解決快照讀的不可重復(fù)讀,快照讀的時機,也會影響讀取的準(zhǔn)確程度。請看下面兩種情況。
下面這種情況讀到的是500。
事務(wù)A | 事務(wù)B |
---|---|
開啟事務(wù) | 開啟事務(wù) |
快照讀(無影響)查詢金額為500 | 快照讀查詢金額為500 |
更新金額為400 | |
提交事務(wù) | |
select 快照讀 金額為500 |
|
select lock in share mode當(dāng)前讀 金額為400 |
下面這種情況讀到的是400。
事務(wù)A
事務(wù)B
開啟事務(wù)
開啟事務(wù)
快照讀(無影響)查詢金額為500
更新金額為400
提交事務(wù)
select
快照讀
金額為400
select lock in share mode
當(dāng)前讀
金額為400
(表格來自[SnailMann]的博客)。
4. 幻讀
幻讀,這個詞本身就非常的迷幻。在RU、RC、RR級別下,都會出現(xiàn)幻讀。
拿一個最簡單的例子來說。讓你select一條記錄是否存在然后打算進行后續(xù)插入時,如果這條記錄不存在,然后你執(zhí)行了插入操作,但在實際執(zhí)行插入操作的時候,結(jié)果卻報錯了,這條記錄已經(jīng)存在了,這就是幻讀。
首先,確認目前時可重復(fù)讀級別。如果不是,則修改之。
- SELECT @@tx_isolation
- # set session transaction isolation level repeatable read
讓我們來看一下這個靈異過程。
有5個步驟,我都給你標(biāo)好了。下面一一介紹。
- 事務(wù)A使用begin開啟一個事務(wù),然后查詢id為3的記錄,此時不存在。但由于快照讀開啟了一個針對于id為3的記錄的read view,所以在這個事務(wù)自始至終都不能夠讀到為3的記錄。很好,這就是我們不可重復(fù)讀所需要的
- 接下來,事務(wù)B插入了一條id為3的記錄,并提交成功
- 事務(wù)A此時也想插入這條記錄,于是執(zhí)行了相同的插入操作,結(jié)果數(shù)據(jù)庫報錯,顯示這條記錄已經(jīng)存在
- 事務(wù)A此時一臉懵逼,想看一下這條記錄到底是啥,但當(dāng)它再次執(zhí)行select語句的時候,卻查不到這條記錄
- 但在其他事務(wù)中,是可以看到這條記錄的,因為它已經(jīng)正確提交
這就是幻讀。
5. 如何解決幻讀
幻讀有錯么?多數(shù)情況下沒錯,就是報錯怪異了些。要防止幻讀,需要開啟FOR UPDATE這樣高強度的鎖定,實際情況是非常少用。
為什么上面的操作,insert能報錯,但select卻無法查到數(shù)據(jù)呢?這就不得不提一下數(shù)據(jù)庫讀的兩種模式:
快照讀:普通的select操作,是從read view中讀取數(shù)據(jù),讀取的可能是歷史數(shù)據(jù)
當(dāng)前讀:insert、update、delete、select..for update這種操作,讀取的總是當(dāng)前的最新數(shù)據(jù)
對于當(dāng)前讀,你讀取的行,以及行的間隙都會被加鎖,直到事務(wù)提交時才會釋放,其他的事務(wù)無法進行修改,所以也不會出現(xiàn)不可重復(fù)讀、幻讀的情形。所以insert能夠發(fā)現(xiàn)沖突,而普通select卻不可以。要想解決幻讀,就需要加X鎖。在上面這種情況,就可以在事務(wù)A中執(zhí)行:
- SELECT * FROM xjjdog_tx WHERE id=3 FOR UPDATE
當(dāng)這么做的時候,即使id為3的記錄不存在,它也會創(chuàng)建鎖(在背后可能根據(jù)記錄的存在與否加行X鎖或者next-key lock間隙x鎖)。
6. 總結(jié)
下面簡單總結(jié)一下。
臟讀,就是一個事務(wù)讀取到另一個事務(wù)還沒有提交的記錄。當(dāng)其他事務(wù)發(fā)生回滾的時候,就會出現(xiàn)問題。
不可重復(fù)讀,意思是在同一個事務(wù)里,讀多次可能會獲得不一致的結(jié)果。這是因為在事務(wù)執(zhí)行期間,有別的事務(wù)修改了這些記錄。
MySQL默認是可重復(fù)讀,但會發(fā)生幻讀的情況?;米x是由于快照讀和當(dāng)前讀的差別產(chǎn)生的。
要想解決幻讀,就需要加鎖(X鎖,Gap鎖等),比如for update,全部改成當(dāng)前讀直到事務(wù)結(jié)束,自然沒有問題。
所謂的最高級別serializable,不過是全部搞成了當(dāng)前讀而已,在高并發(fā)的環(huán)境下效率,可想而知。所以幾乎沒有用的。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。