深入理解MySQL中事務隔離級別的實現原理

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到數據庫事務,大家腦子裏一定很容易蹦出一堆事務的相關知識,如事務的ACID特性,隔離級別,解決的問題(髒讀,不可重複讀,幻讀)等等,但是可能很少有人真正的清楚事務的這些特性又是怎麼實現的,爲什麼要有四個隔離級別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天我們就先來聊聊MySQL中事務的隔離性的實現原理,後續還會繼續出文章分析其他特性的實現原理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然MySQL博大精深,文章疏漏之處在所難免,歡迎批評指正。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"說明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MySQL的事務實現邏輯是位於引擎層的,並且不是所有的引擎都支持事務的,下面的說明都是以InnoDB引擎爲基準。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"定義"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隔離性(isolation)指的是不同事務先後提交併執行後,最終呈現出來的效果是串行的,也就是說,對於事務來說,它在執行過程中,感知到的數據變化應該只有自己操作引起的,不存在其他事務引發的數據變化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"隔離性解決的是併發事務出現的問題"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"標準SQL隔離級別"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隔離性最簡單的實現方式就是各個事務都串行執行了,如果前面的事務還沒有執行完畢,後面的事務就都等待。但是這樣的實現方式很明顯併發效率不高,並不適合在實際環境中使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了解決上述問題,實現不同程度的併發控制,SQL的標準制定者提出了不同的隔離級別:未提交讀(read uncommitted)、提交讀(read committed)、可重複讀(repeatable read)、序列化讀(serializable)。其中最高級隔離級別就是序列化讀,而在其他隔離級別中,由於事務是併發執行的,所以或多或少允許出現一些問題。見以下的矩陣表:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cda954d023dcf5a4bba3f4f71eb2342c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意,MySQL的InnoDB引擎在提交讀級別通過MVCC解決了不可重複讀的問題,在可重複讀級別通過間隙鎖解決了幻讀問題,具體見下面的分析"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現原理"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"標準SQL事務隔離級別實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們上面遇到的問題其實就是併發事務下的控制問題,解決併發事務的最常見方式就是悲觀併發控制了(也就是數據庫中的鎖)。標準SQL事務隔離級別的實現是依賴鎖的,我們來看下具體是怎麼實現的:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/45/45d7fd6f4cf9a594809cde62f425d31c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,在只使用鎖來實現隔離級別的控制的時候,需要頻繁的加鎖解鎖,而且很容易發生讀寫的衝突(例如在RC級別下,事務A更新了數據行1,事務B則在事務A提交前讀取數據行1都要等待事務A提交併釋放鎖)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了不加鎖解決讀寫衝突的問題,MySQL引入了MVCC機制,詳細可見我以前的分析文章:"},{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000023332101","title":null},"content":[{"type":"text","text":"一文讀懂數據庫中的樂觀鎖和悲觀鎖和MVCC"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"InnoDB事務隔離級別實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在往下分析之前,我們有幾個概念需要先了解下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"1、鎖定讀和一致性非鎖定讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"鎖定讀"},{"type":"text","text":":在一個事務中,主動給讀加鎖,如SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。分別加上了行共享鎖和行排他鎖。鎖的分類可見我以前的分析文章:"},{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000025156465#","title":null},"content":[{"type":"text","text":"你應該瞭解的MySQL鎖分類"}]},{"type":"text","text":")。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html","title":null},"content":[{"type":"text","text":"https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"一致性非鎖定讀"},{"type":"text","text":":InnoDB使用MVCC向事務的查詢提供某個時間點的數據庫快照。查詢會看到在該時間點之前提交的事務所做的更改,而不會看到稍後或未提交的事務所做的更改(本事務除外)。也就是說在開始了事務之後,事務看到的數據就都是事務開啓那一刻的數據了,其他事務的後續修改不會在本次事務中可見。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Consistent read是InnoDB在RC和RR隔離級別處理SELECT語句的默認模式。一致性非鎖定讀不會對其訪問的表設置任何鎖,因此,在對錶執行一致性非鎖定讀的同時,其它事務可以同時併發的讀取或者修改它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html","title":null},"content":[{"type":"text","text":"https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"2、當前讀和快照讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"當前讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀取的是最新版本,像"},{"type":"text","marks":[{"type":"strong"}],"text":"UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE"},{"type":"text","text":"這些操作都是一種當前讀,爲什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"快照讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀取的是快照版本,也就是歷史版本,像不加鎖的"},{"type":"text","marks":[{"type":"strong"}],"text":"SELECT"},{"type":"text","text":"操作就是快照讀,即不加鎖的非阻塞讀;"},{"type":"text","marks":[{"type":"strong"}],"text":"快照讀的前提是隔離級別不是未提交讀和序列化讀級別,因爲未提交讀總是讀取最新的數據行,而不是符合當前事務版本的數據行,而序列化讀則會對錶加鎖"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3、隱式鎖定和顯式鎖定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"隱式鎖定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"InnoDB在事務執行過程中,使用兩階段鎖協議(不主動進行顯示鎖定的情況):"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨時都可以執行鎖定,InnoDB會根據隔離級別在需要的時候自動加鎖;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鎖只有在執行commit或者rollback的時候纔會釋放,並且所有的鎖都是在同一時刻被釋放。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"顯式鎖定"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"InnoDB也支持通過特定的語句進行顯示鎖定(存儲引擎層)"}]}]}]},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"select ... lock in share mode //共享鎖\nselect ... for update //排他鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MySQL Server層的顯示鎖定:"}]}]}]},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"lock table\nunlock table"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瞭解完上面的概念後,我們來看下InnoDB的事務具體是怎麼實現的(下面的讀都指的是非主動加鎖的select)"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a6/a69b29e8caa7eb2e1bfc68ce8d492d72.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一些常見誤區"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"幻讀到底包不包括了delete的情況?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可重複讀:前後多次讀取一行,數據內容不一致,針對其他事務的update和delete操作。爲了解決這個問題,使用行共享鎖,鎖定到事務結束(也就是RR級別,當然MySQL使用MVCC在RC級別就解決了這個問題)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幻讀:當同一個查詢在不同時間生成不同的行集合時就是出現了幻讀,針對的是其他事務的insert操作,爲了解決這個問題,鎖定整個表到事務結束(也就是S級別,當然MySQL使用間隙鎖在RR級別就解決了這個問題)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網上很多文章提到幻讀和提交讀的時候,有的說幻讀包括了delete的情況,有的說delete應該屬於提交讀的問題,那到底真相如何呢?我們實際來看下MySQL的官方文檔(如下)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a "},{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000025156465#","title":null},"content":[{"type":"text","text":"SELECT"}]},{"type":"text","text":") is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html","title":null},"content":[{"type":"text","text":"https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,幻讀針對的是結果集前後發生變化,所以看起來delete的情況應該歸爲幻讀,但是我們實際分析下上面列出的標準SQL在RR級別的實現原理就知道,標準SQL的RR級別是會對查到的數據行加行共享鎖,所以這時候其他事務想刪除這些數據行其實是做不到的,所以在RR下,不會出現因delete而出現幻讀現象,也就是幻讀不包含delete的情況。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"MVCC能解決了幻讀問題?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網上很多文章會說MVCC或者MVCC+間隙鎖解決了幻讀問題,實際上MVCC並不能解決幻讀問題。如以下的例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"begin;\n\n#假設users表爲空,下面查出來的數據爲空\n\nselect * from users; #沒有加鎖\n\n#此時另一個事務提交了,且插入了一條id=1的數據\n\nselect * from users; #讀快照,查出來的數據爲空\n\nupdate users set name='mysql' where id=1;#update是當前讀,所以更新成功,並生成一個更新的快照\n\nselect * from users; #讀快照,查出來id爲1的一條記錄,因爲MVCC可以查到當前事務生成的快照\n\ncommit;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到前後查出來的數據行不一致,發生了幻讀。所以說只有MVCC是不能解決幻讀問題的,解決幻讀問題靠的是間隙鎖。如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"begin;\n\n#假設users表爲空,下面查出來的數據爲空\n\nselect * from users lock in share mode; #加上共享鎖\n\n#此時另一個事務B想提交且插入了一條id=1的數據,由於有間隙鎖,所以要等待\n\nselect * from users; #讀快照,查出來的數據爲空\n\nupdate users set name='mysql' where id=1;#update是當前讀,由於不存在數據,不進行更新\n\nselect * from users; #讀快照,查出來的數據爲空\n\ncommit;\n\n#事務B提交成功並插入數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意,RR級別下想解決幻讀問題,需要我們顯式加鎖,不然查詢的時候還是不會加鎖的"},{"type":"text","text":"。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章