鎖、事務的隔離級別、事務的併發問題——寫好“增刪改查”必須知道的MySQL InnoDB相關知識
MySQL InnoDB
MySQL InnoDB是最常用的MySQL數據庫引擎,這篇文章以後端程序員的視角對他做一些介紹,不會牽扯實現原理,也不會引入太多名詞,免得大家越看越糊塗。
鎖的類型
共享鎖(SLock)
結論:共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是隻能讀不能修改。SQL語句:SELECT … LOCK IN SHARE MODE
驗證:
第一步,我們先開啓事務,給id爲11的記錄加共享鎖;
第二步,打開另外一個命令行,執行查詢和修改,不管聲不聲明事務,結論都一樣:查詢可以成功,修改失敗;
第三步,提交第一步中開啓的事務,釋放共享鎖;
第四步,剛纔執行失敗的update語句再執行一遍,成功執行
排他鎖(XLock)
結論:允許事務刪除或更新一行數據。排他鎖就是不能與其他鎖並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的鎖,包括共享鎖和排他鎖。獲取排他鎖的事務是可以對一行數據讀取和修改。SQL語句:SELECT … FOR UPDATE
驗證:
第一步,開啓事務,給id爲12的記錄加排他鎖;
第二步,打開另外一個命令行,執行查詢,可以成功;嘗試獲取共享鎖或排他鎖,全都失敗;update和delete也都不能成功;
第三步,提交第一步中開啓的事務,釋放排他鎖;
第四步,對id爲12的數據加排他鎖,可以成功,數據更新當然也是可以成功的。
鎖的範圍
鎖行
結論:當查詢條件帶主鍵或索引,且記錄存在時,鎖住匹配到的那一行
驗證:
第一步,開啓事務,給id爲12的那一行加排他鎖,id是主鍵;
第二步,向user表插入一條數據,可以成功插入;
第三步,提交第一步中開啓的事務,釋放排他鎖;
第四步,開啓事務,給name爲‘hello’的那一行加排它鎖,其中name加了唯一索引;
第五步,向user表插入一條數據,可以成功插入;
鎖表
結論:當查詢條件不是主鍵或索引,或者主鍵或索引匹配不到任何數據時,會鎖住整張表
驗證:
第一步,開啓事務,給id爲100的行加排他鎖,我們可以看到id爲100的數據是不存在的;
第二步,開啓另外一個命令行,向user表插入一條數據,發現插入失敗;
(把id換成索引字段name來驗證也可以得到一樣的結論)
第三步,給user表新增一個普通字段sex,開啓事務,以sex=‘男’爲查詢條件加共享鎖;
第四步,打開另外一個命令行,將sex=‘女’的記錄的sex改爲‘中性’,執行失敗;
讀的類型
一致性非鎖定讀(快照讀)
如果讀取的行正在執行delete或update操作,這時讀取操作不會因此去等待行上的鎖釋放,而是去讀取一個快照數據。快照數據是指該行之前版本的數據,該實現是通過undo段來完成的,而undo用來在事務中回滾數據。此外,讀取快照數據是不需要上鎖的,因爲沒有事務需要對歷史的數據進行修改操作。在默認配置下,即事務隔離級別爲REPEATABLE READ(可重複讀)時,MySQL InnoDB使用一致性非鎖定讀。
一致性鎖定讀(當前讀)
對於select語句可以加鎖來實現一致性的鎖定讀,可以加共享鎖,也可以加排他鎖。
事務的隔離級別
READ UNCOMMITTED
瀏覽訪問,俗稱“讀未提交”。會出現“髒讀”;這種隔離級別最低,一般是在理論上存在,很少用到
READ COMMITTED
遊標穩定,俗稱“讀提交”。這種隔離級別高於讀未提交;可以避免“髒讀”;但會導致“不可重複讀”
REPEATABLE READ
俗稱“重複讀”。這種隔離級別高於“讀已提交”;可以實現可重複讀(通過上文提到的一致性非鎖定讀實現的),不能避免“幻讀”
SERIALIZABLE
隔離,俗稱“串行化”。在此事務隔離級別,InnoDB存儲引擎會對每個select語句後自動加上lock in share mode,因此對一致性的非鎖定讀不再予以支持。(ps:有的博客說SERIALIZABLE加的是排他鎖,我查資料發現並不是)
事務的併發問題
髒讀
指的是在不同的事務中,當前事務可以讀到另外事務未提交的數據。比如:事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據。
不可重複讀
是指在一個事務內多次讀取同一數據集合,在這個事務還沒有結束時,另外一個事務也訪問了該同一數據集合,並做了一些DML操作。因此,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麼第一個事務兩次讀到的數據可能是不一樣的。
幻讀
“幻讀”跟“不可重複讀”不是同一個問題,REPEATABLE READ不能解決“幻讀”問題。“幻讀”是指一個事務A中,先select查詢符合條件1的數據是否存在,發現不存在再insert,結果在此時,另外一個事務B insert了一條符合條件1的數據,這就導致我在事務A中的select看到的就像幻覺一樣。可以加排他鎖(XLock)來解決“幻讀”的問題。
(PS:扒一扒我接手的一個老系統遇到的“幻讀”坑。就是某個表的郵箱字段,業務上這個郵箱字段應該是唯一的,但是數據表設計時沒有加唯一約束,然後這個校驗是否唯一的工作就放到代碼中來做了,總之就是先查詢,發現不存在就insert,然而這段代碼並沒有加鎖。平時用着也沒什麼問題,因爲訪問量不高,但有一天不知是什麼原因,同一類型的請求在幾毫秒之內發了兩次,然後就發生瞭如下情況:第一個請求進來查詢發現郵箱地址不存在,於是就insert了一條包含該郵箱的數據;第二個請求以幾毫秒的間隔進入該方法,查詢發現該郵箱不存在,我看日誌發現此時第一個請求已經insert完成了,只不過還沒完成事務的提交,所以第二個請求的事務中是查詢不到另外一個事務中未提交的數據(數據庫隔離級別是默認的REPEATABLE READ),於是也insert了一條一模一樣的數據)
丟失更新
簡單來說就是一個事務的更新操作會被另一個事務的更新操作所覆蓋,從而導致數據的不一致。不過,在當前數據庫的任何隔離級別下,都不會導致數據庫理論上的丟失更新問題。這是因爲,即使是READ UNCOMMITTED的事務隔離級別,對於行的DML操作,需要對行或其他粗粒度級別的對象加鎖。不論你的事務隔離級別是什麼樣的,只要你開啓事務修改了一行數據還沒有提交,你就獲得了這行數據的寫鎖,其他事務就不能再修改這條記錄。事務隔離級別只是確認其他事務能不能讀取到你修改過但未提交的數據記錄而已,當你修改完成提交事務,其他事物才能獲得這行記錄的修改權限,才能執行修改操作。