原文地址:https://zhuanlan.zhihu.com/p/150107974
一般對於我們的業務系統去訪問數據庫而言,它往往是多個線程並發執行多個事務的,對於數據庫而言,它會有多個事務同時執行,可能這多個事務還會同時更新和查詢同一條數據,所以這里會有一些問題需要數據庫來解決
我們來看看,如果多個事務要是對緩存里的同一條數據同時進行更新或者查詢,此時會產生哪些問題呢?這里實際上會涉及到臟寫、臟讀、不可重復讀、幻讀四種問題。
臟寫
臟寫,意思是說有兩個事務,事務 A 和事務 B 同時在更新一條數據,事務 A 先把它更新為 A 值,事務 B 緊接着就把它更新為 B 值。如圖:
可以看到,此時事務 B 是后更新那行數據的值,所以此時那行數據的值是 B。而且此時事務 A 更新之后會記錄一條 undo log 日志。因為事務 A 是先更新的,它在更新之前,這行數據的值為 NULL
。所以此時事務 A 的 undo log 日志大概就是:更新之前這行數據的值為 NULL,主鍵為 XX
那么此時事務 B 更新完數據的值為 B,此時事務 A 突然回滾了,就會用它的 undo log 日志去回滾。此時事務 A 一回滾,直接就會把那行數據的值更新回 NULL 值。如圖:
然后就尷尬了,事務 B 一看,為什么我更新的 B 值沒了?就因為你事務 A 反悔了把數據值回滾成 NULL 了,結果我更新的 B 值也不見 了。所以對於事務 B 看到的場景而言,就是自己明明更新了,結果值卻沒了,這就是臟寫。
所謂臟寫,就是我剛才明明寫了一個數據值,結果過了一會卻沒了。而它的本質就是事務 B 去修改了事務 A 修改過的值,但是此時事務 A 還沒提交,所以事務 A 隨時會回滾,導致事務 B 修改的值也沒了,這就是臟寫的定義。
臟讀
假設事務 A 更新了一行數據的值為 A 值,此時事務 B 去查詢了一下這行數據的值,看到的值是 A 值,如圖:
接着,事務 B 拿着剛才查詢到的 A 值做各種業務處理。但是接着坑爹的事情發生了,事務 A 突然回滾了事務,導致它剛才功能的 A 值沒了,此時那行數據的值回滾為 NULL 值。然后事務 B 緊接着此時再次查詢那行數據的值,看到的居然是 NULL 值。如圖:
這就是臟讀。它的本質是事務 B 去查詢了事務 A 修改過的數據,但是此時事務 A 還沒提交,所以事務 A 隨時會回滾導致事務 B 再次查詢就讀不到剛才事務 A 修改的數據了,這就是臟讀。
其實總結一句話,無論是臟寫還是臟讀,都是因為一個事務去更新或者查詢了另外一個還沒提交的事務更新過的數據。因為另外一個事務還沒提交,所以它隨時可能會回滾,那么必然導致你更新的數據就沒了,或者你之前查詢到的數據就沒了,這就是臟寫和臟讀兩種場景。
不可重復讀
假設我們有一個事務 A 開啟了,在這個事務 A 里會多次對一條數據進行查詢。然后呢,另外有兩個事務,一個是事務 B,一個是事務 C,他們兩都是對一條數據進行更新的。然后我們假設一個前提,就是比如說事務 B 更新之后,如果還沒提交,那么事務 A 是讀不到的,必須要事務 B 提交之后,它修改的值才能被事務 A 讀取到,其實這種情況下,就是我們首先避免了臟讀的發生
因為臟讀的意思就是事務 A 可以讀到事務 B 修改過還沒提交的數據,此時事務 B 一旦回滾,事務 A 再次讀就讀不到了,那么此時就會發生臟讀問題。我們現在假設的前提是事務 A 只能在事務 B 提交之后讀取到它修改的數據,所以此時必然是不會發生臟讀的
但是,此時會有另外一個問題,叫做不可重復讀。假設緩存頁里一條數據原來的值是 A 值,此時事務 A 開啟之后,第一次查詢這條數據,讀取到的就是 A 值。如圖:
接着事務 B 更新了那行數據的值為 B 值,同時事務 B 立馬提交了,然后事務 A 此時還沒提交。大家注意,此時事務 A 是沒提交的,它在事務執行期間第二次查詢數據,此時查到的是事務 B 修改過的值,B 值,因為事務 B 已經提交了,所以事務 A 是可以讀到的,如圖:
緊接着事務 C 再次更新數據為 C 值,並且提交事務了,此時事務 A 在還沒提交的情況下,第三次查詢數據,查到的值為 C 值,如下:
那么上面的場景有什么問題呢?其實要說沒問題也可以是沒問題的,畢竟事務 B 和 事務 C 都提交之后,事務 A 多次查詢查到它們修改的值,是 OK 的。但是你要說有問題,也可以是有問題的,就是事務 A 可能第一次查詢到 A 值,那么它可能希望的是在事務執行期間,如果多次查詢數據,都是同樣的一個 A 值,它希望這個 A 值是它重復讀取的時候一直可以讀到的。它希望這行數據的值是可重復讀的
但是此時,明顯 A 值是不可重復讀的。因為事務 B 和事務 C 一旦更新值並且提交了,事務 A 會讀到別的值,所以此時這行數據的值是不可重復讀的。此時對於你來說,這個不可重復讀的場景,就是一種問題
上面描述的,其實就是不可重復讀的問題,其實這個問題你說是問題也不一定就是什么大問題。因為這取決於你自己想要數據庫是什么樣子的,如果你希望看到的場景是不可重復讀,也就是事務 A 在執行期間多次查詢一條數據,每次都可以查到其它已經提交的事務修改過的值,那么就是不可重復讀,如果你希望這樣子,那也沒問題。
如果你期望的是可重復讀,但是數據庫表現的是不可重復讀,讓你事務 A 執行期間多次查到的值都不一樣,都的問題是別的提交過的事務修改過的,那么此時你就可以認為,數據庫有問題,這個問題就是「不可重復讀」
幻讀
臟寫、臟讀和不可重復讀都分別代表了不同的數據庫問題。臟寫就是兩個事務沒提交的狀況下,都修改同一條數據,結果一個事務回滾了,把另外一個事務修改的值也撤銷了,所謂臟寫就是兩個事務沒提交狀態下修改同一個值。
臟讀就是一個事務修改了一條數據的值,結果還沒提交呢,另外一個事務就讀到了你修改的值,然后你回滾了,人家事務再次讀,就讀不到了,即人家事務讀到了你修改之后還沒提交的值,這就是臟讀了。而不可重復讀,針對的是已經提交的事務修改的值,被你事務給讀到了,你事務內多次查詢,多次讀到的是別的已經提交的事務修改過的值,這就導致不可重復讀。
接着我們說說幻讀。簡單來說,你一個事務 A,先發送一條 SQL 語句,里面有一個條件,要查詢一批數據出來,如 SELECT * FROM table WHERE id > 10
。然后呢,它一開始查詢出來了 10 條數據。接着這個時候,別的事務 B往表里插了幾條數據,而且事務 B 還提交了,此時多了幾行數據。如圖:
接着事務 A 此時第二次查詢,再次按照之前的一模一樣的條件執行 SELECT * FROM table WHERE id > 10
這條 SQL 語句,由於其他事務插入了幾條數據,導致這次它查詢出來了 12 條數據。如圖:
於是事務 A 開始懷疑自己的眼鏡了,為什么一模一樣的 SQL 語句,第一次查詢是 10 條數據,第二次查詢是 12 條數據?難道剛才出現幻覺了?這就是「幻讀」這個名詞的由來
幻讀就是你一個事務用一樣的 SQL 多次查詢,結果每次查詢都會發現查到一些之前沒看到過的數據。注意,幻讀特指的是你查詢到了之前查詢沒看到過的數據。此時說明你是幻讀了
其實,臟寫、臟讀、不可重復讀、幻讀,都是因為業務系統會多線程並發執行,每個線程可能都會開啟一個事務,每個事務都會執行增刪改查操作。然后數據庫會並發執行多個事務,多個事務可能會並發地對緩存頁里的同一批數據進行增刪改查操作,於是這個並發增刪改查同一批數據的問題,可能就會導致我們說的臟寫、臟讀、不可重復讀、幻讀這些問題。
所以這些問題的本質,都是數據庫的多事務並發問題,那么為了解決多事務並發問題,數據庫才設計了事務隔離機制、MVCC 多版本隔離機制、鎖機制,用一整套機制來解決多事務並發問題。