InnoDB在RR隔離級別下解決幻讀問題

表象:快照讀(非阻塞讀)—僞MVCC
內在:next-key鎖(行鎖+gap鎖)

當前讀和快照讀

當前讀:select…lock in share mode(共享鎖),select…for update(排他鎖)
當前讀:update,delete,insert(排他鎖)

當前讀就是加了鎖的增刪改查語句,無論是上的共享鎖還是排他鎖均爲當前讀,讀取的爲當前的最新版本並且讀取之後還要保證其他併發事務不能修改當前記錄,對讀取的記錄加鎖RDBMS主要由兩部分組成(程序實例和存儲(InnoDB))

以update語句爲例:當update SQL發給Mysql之後,MysqlSever會根據where條件,讀取第一條滿足條件的記錄(select row 1)innoDB引擎會將第一條記錄返回並加鎖(return&lock)待mysqlsever接收到這條加鎖的記錄後會發起一個update操作去更新這條記錄,一條記錄更新完成了之後再讀取下一條記錄直至沒有滿足條件的記錄爲止,update操作就包含了一個當前讀來獲取數據的最新版本,就跟在readcommitted下出現的這個幻讀的情況一樣由於先前另外一個事務新提交了一個數據當前事務update全表的時候就莫名多了一條數據即讀取到了數據的最新版本,同理DELETE操作也一樣,insert操作會稍有不同,簡單的來說insert操作可能會觸發唯一鍵的衝突檢查也會進行一個當前讀

快照讀:不加鎖的非阻塞讀,select(不加鎖的條件是以在事務隔離級別不爲Serializable的前提下才成立的,由於serializable是串行讀,所以此時的快照讀也退化爲當前讀,即select…lock in share mode 模式,之所以出現快照讀是基於提升併發效率的考慮,快照讀的實現是基於多版本的併發控制即MVCC,可以認爲MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖的操作,因此開銷更低,既然是基於多版本,也就意味着快照讀可能讀到的並不是最新版本的數據,可能是之前的歷史版本)

InnoDB的非阻塞讀是在RC/RR隔離級別下是如何實現的

三個因子
1.數據行裏的
DB_TRX_ID(標識最近一次對本行數據做修改,無論是insert,update,事務的標識符,即最後一次修改本行數據的事務ID,delete在innoDB看來也是一次update操作,更新行中的一個特殊位,將行標識爲deleted,也就是說數據行中除了這三列,還有一個被稱爲deleted的隱藏列)
DB_ROLL_PTR(回滾指針,指寫入回滾段ROLLBACK SEGMENT的undo日誌記錄,如果一行記錄被更新,則undolockrecalled包含從建該行記錄被更新之前內容所必須的信息)
DB_ROW_ID(行號,包含一個隨着新行插入而單調遞增的行ID,當由innoDB自動產生具體索引時,具體索引會包括這個行id的值,否則這個行ID不會出現在任何索引中)字段

2.undo日誌:當我們對記錄做了變更操作時,就會產生undo記錄,undo記錄中存儲的是老版的數據,當一箇舊的事務需要讀取數據時,爲了能讀取到老版本的數據,需要順着undo鏈找到滿足其可見性的記錄,undolog主要分爲兩種insertundolog,updateundolog,其中insertundolog表示事務對insert新記錄產生的undolog,只在事務回滾時需要並且在事務提交後就可能會立即丟棄。updateundolog事務對記錄進行delete或者update時產生的undolog,不僅在事務回滾時需要,快照讀也需要所以不能刪除,只有數據庫所使用的快照中不涉及該日誌記錄對應的回滾日誌纔會被刪除

3.read view:主要用來做可見性判斷的,即當我們做快照讀select的時候會針對我們所查詢的數據創建出一個read view來決定當前事務能看到的是哪個版本的數據,有可能是當前最新的數據,也有可能只允許你看undolog裏面某個版本的數據,遵循可見性算法。主要是將要修改的數據的DB_TRX_ID取出來,與系統其他活躍ID做對比如果大於或者等於這些ID的話,就通過DB_ROLL_PTR指針去取出undolog上一層的DB_TRX_ID直到小於這些活躍事務的ID爲止,這樣就保證了我們獲取的數據版本是是當前最穩定的版本

每當我們start transaction的時候事務ID都會去遞增,也就是說越新開啓的事務這個ID就會越大由於生成是時機不同,造成了RC,RR兩種事務隔離級別的可見性不同

在Repeatable read隔離級別下,session Strat transaction後的第一條快照讀會創建一個快照即read view ,將當前活躍的其他事務記錄起來,此後再調用快照讀的時候還是用的是同一個read view

在 read committed級別下,事務中每條 select語句每次調用快照讀的時候都會創建新的快照,這就是爲什麼我們在此隔離級別下能用快照讀看到其他事務已提交的對錶的增刪改了,而在RR下如果首次使用快照讀是在別的事務對數據進行增刪改提交之前的,此後即便別的事務對數據做了增刪改並提交,還是讀不到數據變更的原因(首次select的時機很重要)

由於以上的三個因子才使得innoDB在RR或者RC級別下支持非阻塞讀,而讀取數據時的非阻塞就是所謂的MVCC(Multi-Version Concurrency Control多版本控制),而InnoDB的非阻塞讀機制實現的仿製版的MVCC,並沒有實現MVCC的核心的多版本共存,undolog中的內容只是串行化的結果,記錄了多個事務的過程,不屬於多版本共存讀不加鎖,讀寫不衝突,在讀多寫少的應用中讀寫不衝突是非常重要的,極大的增加了系統的併發性能,快照讀並非是幻讀現象發生的根本,只是你如果先要提交數據變更的事務,打開read view時不論別的事務的變更是否已提交,在當前事務內再次調用快照讀的時候還是讀的可見性版本內的數據,有一種掩耳盜鈴的意思在裏面。而真正防止幻讀發生的原因是事務對數據加了next-key鎖。

next-key鎖

1.行鎖:是對單獨行記錄上的鎖,

2.gap鎖:Gap是索引樹中插入新記錄的空隙,而gaplock間隙鎖即鎖定一個範圍但不包括記錄本身,gap鎖的目的是爲了防止事務的兩次當前讀出現幻讀的情況gap在RC隔離級別或者更低的隔離級別下是沒有的,這就是RC等隔離級別無法避免幻讀的原因,而在RR以及serializable級別下默認都支持gap鎖

對主鍵索引或者唯一索引會引用Gap鎖嗎

視情況而定
1.如果where條件全部命中,則不會使用gap鎖,只會加記錄鎖(精確查詢所有記錄都有例如 select * from table where id in(1,5,7) 157數據均在此table存在並且出現,就是全部命中,如果只查到了部分,則爲部分命中)

假設id爲主鍵或者唯一鍵,那麼在事務A中我們將id作爲where篩選條件去做當前讀的時候,比如delete from table where id = 9 ,此時事務B新增了一條記錄,必然也會出現在這個當前讀的範圍之外,所以在事務B新增數據並提交了之後事務A再去做當前讀還是獲取到原來的數據集,並不會產生所謂的幻讀現象,所以此時加行鎖就足夠了,鎖住這寫ID唯一的特定行便可以防止另外的事務對該結果集做出的影響。也就是說沒有加gap鎖的必要,值得注意的是,加鎖的時候如果我們走的是主鍵之外的索引,那麼我們需要對當前索引,以及主鍵索引上對應的記錄都上鎖,

2.如果where條件部分命中或者全不命中,則會加Gap鎖

gap鎖會用在非唯一索引或者不走索引的當前讀中

當前讀:1.非唯一索引(使用Gap防止幻讀的發生)

例如select … from tabel where id = 8 (tabel中有兩條滿足條件的數據,name爲primary key,id key),在執行詞語句的過程中未提交,另一個事務插入了一條id爲8的其他數據,此時當前讀的數據則會變爲三條,這樣就會發生幻讀,此時我們就要引入gap鎖。(具體詳見官方文檔 左開右閉區間,在這些區間內一旦上了gap鎖,該區間就無法插入數據了),因此gap鎖是爲了防止插入的,對於普通非唯一索引並不是所有的gap都會去上鎖,只會對要修改的地方的周邊上gap鎖。主鍵的值也起到了一定的作用

2.不走索引(噹噹前讀不走索引的時候,他對所有的gap都上鎖,也就類似鎖表,可以達到防止幻讀的效果,相比表鎖,這種上鎖的方式代價更大,需要避免)

innoDB的RR級別主要通過引入next-key鎖來避免幻讀問題,而next-key由recorelock和gaplock組成,gaplock會用在非唯一索引或者不走索引的當前讀以及僅命中檢索條件的部分結果集,並且用到主鍵索引,以及唯一索引的當前讀中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章