從本源來理解比較容易理解,如果只是描述概念和定義,容易讓人雲里霧里找不到方向.正好這兩天在瀏覽mysql的文檔,我可以簡單在這里總結一下,幫助其他還沒有理解的朋友,如果有錯誤也麻煩幫忙指正.
先講一點背景知識:
首先明確一點,數據庫的命令的執行者的封裝基本抽象是Transaction,語句的執行都會有對應的Transaction對象,並且都會有對應的id來標識不同的Transaction.Transaction Id按照不同的時序來分配, 通俗點說,時間上來執行的早的transaction id小一點, 晚的transaction id大一點.不是說只有我們手動去start transaction這樣才會創建transaction.我們執行一個簡單的autocommit類型的語句,比如insert或者update語句,都會對應生成一個新的事務.
所以,所有SQL語句執行皆有其對應的transaction id, transaction id的大小可以表示SQL語句執行發生的早晚.
Mysql提供了多版本讀取能力.什么叫多版本?就是說表中一份存儲的數據行會有多個版本,這個版本對應的version就是transaction id.當行數據被某個transaction變更時, 這個行會有個隱含的hidden column中記錄了這個更新者的transaction id.舊的數據在被覆蓋的同時,會存儲到一個undolog的文件中,這個文件中就是保存着每一行的版本數據鏈表,沿着這個鏈表走我們可以走過這一行數據之前的版本變更.
(這個undolog數據量不需要那么大,比如一個行數據的更新者transaction id小於所有server中活躍的transaction ids,那么這個行數據的undolog中的數據就可以刪除掉,因為不會有transaction來去查詢這個行記錄的undolog)
一,現在來說什么是可重復讀:
當開始一個新的transaction,這個transaction會分配一個新的transactionid,此時在這個transaction內部如果我們執行一條普通的select語句,根據select語句的condition,我們會找到一些滿足條件的行記錄,先不着急返回:
1.如果此時是mysql默認的repeatable read隔離模式
每個匹配的行記錄,我們從前面說的hidden column中找到對應的最近一次執行更新操作的操作者transaction id,我們將這個id(也就是版本),和我們當前的transaction id進行比較,如果大於執行語句所屬的transaction的id,那么就需要去undolog文件中去尋找舊的版本,一直找到小於當前transaction id的版本的行記錄,這個就作為快照數據返回.其他的行記錄都是這樣來處理的.
在repeatable read隔離模式中,所有的普通select語句(consistent read)(非select ... for update的語句)都會進行這樣的比對過程來返回數據.也就是說,即使在我們當前這個事務執行過程中,有其它事務執行插入了新的數據能夠滿足我們的查詢條件,但因為這個數據的版本(transaction id)大於我們當前事務的id,我們是查詢不到這個數據的.
這也就是成為可重復查的原因,不管多少次查詢,每次讀取的都是同樣的版本的數據,也就是字面的意思.
2.如果此時是read committed隔離模式
對於普通的查詢select語句不再有上面這個版本的限制,每次查詢,只需要返回滿足條件的最新的行記錄即可.所以此時就不是可重復讀,也就是說我們可以看到別的事務提交的數據(所以名字叫read committed嘛)
二, 然后來說什么是幻讀:
幻讀的字面意思很簡單,就是在事務內(這里強調同一個事務)兩次查詢語句(注意是select ...for...update語句,不是前面的普通查詢語句<consistent read>),讀取到了不同的數據.
這個從情理上來看是很正常的時情,首先因為數據庫是一個支持大量並發任務的服務,那么我們在事務執行的過程中,新的數據插入並發生變化也是很正常的事情,所以這不是BUG.
注意,這里說到了並發, 在編程語言里面我們知道,出現了並發問題的一個最最常見的情況就是,check and do這樣的操作,這樣不是並發操作安全的,因為check和do的過程中可能會插入其它的操作,所以當我們進行do操作的時候,先前的條件可能已經不滿足了.
那么在編程語言中我們是怎么解決問題的呢?
也就是加鎖,對我們關注的數據(面向對象語言中的對象)進行加鎖操作,其實也就是獨占,我這個任務在拿着這個鎖的時候,別的人都靠邊站(都阻塞在那里),我這個任務做完了,其它任務才可以去做.
在數據庫中也是同樣的問題,我們的select..where..condition..for update也是先圈定一個感興趣的數據(滿足condition的行數據),然后進行update操作,在這個事務進行過程中面臨的也是並發的問題.
那么參考編程語言的做法,我們的方式也是獨占,獨占的目標是什么呢?
只是滿足條件的行數據嗎?
當然不止,比如condition: a>5 and a<10,目前只有一條記錄a=7,如果我們只鎖這個記錄可以嗎?不可以,因為其它事務可能還會做新的插入操作,比如插入一條a=9的記錄,假如我們的業務邏輯是,如果當前a在5-10的區間中只有一條記錄,我們就可以刪除這個記錄(這個業務邏輯很奇怪,先假設是這樣),在上一次查詢的時候是只有一條行記錄,我們認為滿足了條件,現在我們就要刪除這個記錄,卻發現5-10中有了2條記錄,那么此時的刪除操作就是一個誤操作....
數據庫解決這個問題的辦法就是,對這個區間都加鎖,不僅僅是已有的行記錄,空的那些a的值[8,9]都會加上鎖.也就是gap lock,這個時候我們當前的事務就達到了對這個區間獨占的目的,其它的事務在我們處理過程中就無法插入新的數據了: ).
當然, 是否獨占,加不加區間的鎖,這些mysql給我們了自由選擇的權力,在READ COMMITED下,就不會加上區間鎖,只會鎖住已有的記錄,所以此時如果有其它事務插入新的數據,當然也可以成功.如果在REAPEATABLE-READ下,就是會執行上面說的獨占操作,其它想在這個區間插入數據的事務就得等在那里了(阻塞住了).
講到這里,我覺得幻讀和可重復讀的關系應該理清了,
1.repeatable read模式下,我們執行select...for..update獨占了滿足條件的記錄,阻斷了其它事務的變更,不會出現幻讀的情況.
在read committed模式下,不獨占,會有並發問題,會出現查到新的數據的問題.
阻斷幻讀的目的往往在於我們有check-and-do這樣的業務邏輯需求,來實現更進一步嚴格的原子性需求.
2.對於可重復讀,是數據庫多版本的一種福利,幫助我們實現在事務中能夠實現鎖定時間點讀取快照的目的.
最后說一個區間鎖的場景,
比如我們需要業務場景中經常會有類似這樣一個需求,當表中存在滿足某個條件的一行數據,我們就不做.如果不存在我們就插入一條記錄.此時,我們就可以利用上面說的解決幻讀的辦法,使用repeatable模式,因為它會鎖定對應的條件,在我們select...for..update過程中,可以保證不會有其它事務插入.這樣我們的表中就不會出現因為並發問題,導致無法實現唯一性的問題了.
讀者點評一下上述文章的幾個核心點
1.首先對於普通的增、修改、刪除操作MYSQL默認開啟一個事務;不要以為沒有事務開啟,只是完成操作就結束了
2.如果數據庫開啟了可重復讀的隔離模式,那就需要知道以下幾個事情:
1)快照讀
我們執行一條普通的select查詢語句由於默認會開啟一個新事務,分配一個事務ID,那么mysql會去判斷(undo文件)自己是不是事務ID最小的,如果不是,那么盡管有比自己大的,有可能並發下,有新的數據插入,那也不管,只返回比自己小的數據!
2)間隙鎖
對於select ...for update 如果是區間查詢 比如 a>5 and a<10 mysql做法是啟用間隙鎖,它會選擇鎖住一個區間范圍;此時是一個獨占的概念,另外的事務插入、修改都沒辦法進行,需要等待這個事務被釋放,起到一個串行的概念!
作者:扣鼎之歌
鏈接:https://www.zhihu.com/question/38507762/answer/968486962
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。