- MySQL中的鎖與鎖策略
在MySQL中,爲了應對併發場景下的讀寫,鎖通常分爲兩類:共享鎖以及排他鎖。其中,共享鎖允許多個連接在同一時間併發的讀取相同的資源,彼此之間互不影響,所以又稱爲讀鎖。排他鎖則會阻塞其他嘗試獲取共享鎖或者排他鎖的操作,確保同一時間只有一個連接可以寫入數據,並禁止其他用戶的讀寫,又稱寫鎖。
在實際使用下,加鎖往往意味着高昂的開銷,MySQL爲了平衡鎖的開銷以及併發的線程之間的安全,採用了兩種不同的鎖策略:
- table lock(表鎖)
表鎖會鎖定整張表,如果當前有用戶正在執行寫操作並且獲取了寫鎖,這可能導致整張表被鎖定,阻塞其他用戶的讀寫操作。如果用戶執行的是讀操作,則會獲取讀鎖,此時其他用戶的併發讀操作將被接受,寫操作會被阻塞。
舉個例子,執行語句
:
如果b字段不存在索引,那麼會鎖住所有的記錄,即鎖上了表鎖。
- row lock(行鎖)
行鎖的粒度是在每一條行數據,這意味行鎖可以儘可能的支持併發處理,相應的行鎖
開銷也會比較大。並且,在InnoDB中的行鎖是針對索引加的鎖,不是針對記錄加的鎖,並且該索引不能失效,否則行鎖將會自動升級爲表鎖。
相比較而言,表鎖的優勢在於開銷小,加鎖快,無死鎖,劣勢是鎖的粒度大,發生鎖衝突的概率較高,併發能力較弱。而行鎖則相反。實際使用中,兩者都會由MySQL自動加鎖。行鎖衝突可以通過執行 show status like 'innodb_row_lock%'語句進行分析,表鎖衝突則可通過執行show status like 'table_locks%' 進行查看。
- MySQL中的事務與隔離級別
事務就是一組原子性的sql,要麼MySQL引擎會全部執行這一組sql語句,要麼全部不
行(不允許任何一條失敗)。失敗的語句將導致事務的整個回滾。事務系統通常滿足四個特性,分別爲原子性(要麼全部執行、要麼全部回滾)、一致性(數據必須從一個一致性狀態轉換爲另一種一致性狀態)、隔離性(事務未執行成功,其他人無法看到結果)、持久性(事務在commit之後,數據不會丟失)。
由上述概念可知,事務是用來保障數據的一致性以及完整性的。也是MySQL中用來平衡效率與安全之間的一種手段,所以,InnoDB引擎下的事務通常提供了四種事務的隔離級別,方便用戶自己在效率和安全之間做出權衡。
- READ UNCOMMITED(未提交讀)
事務中的修改,即使該事務未提交,對其他的事務也是可見的。可以讀取到其他事務
中的數據,又稱爲髒讀,在實際數據庫事務中,髒讀會破壞數據的一致性,對業務產生極大影響,所以一般不推薦採用READ UNCOMMITED作爲數據庫事務的隔離級別。
- READ COMMITED(提交讀)
在提交讀級別中,數據庫將保證如果一個事務沒有完全執行成功(commit完成),事務中的操作對其他的事務是不可見的。在該隔離級別下,雖然杜絕了髒讀的發生,但是還是存在着不可重複讀以及幻讀的問題。不可重複讀發生在事務T1讀取了一行數據,事務T2接着修改或者刪除了該行數據(已提交),當T1事務再次讀取同一行數據的時候,發現數據已經被修改或者被刪除。示例如下圖:
幻讀則發生在事務T1讀取了滿足某條件的一個數據集,事務T2此時插入了一行或者多行滿足T1查詢條件的的數據並提交,當T1再次採用相同的條件進行讀取時,得到了與第一次不同的結果集。示例如下:
- REPEATABLE READ(提交讀)
REPEATEABLE READ是MySQL的默認隔離級別,它確保同一個事務的多個實例在並
發讀取數據時,會看到同樣的數據。按照該隔離級別定義,還是會存在幻讀的問題。MySQL通過InnoDB存儲引擎的多版本併發控制機制(MVCC)解決了部分該問題的場景,但仍然存在,此處後文會另有分析。
- SERIALIZABLE(串行化)
串行化是最嚴格的隔離級別,通過給事務中的每次讀寫操作都加鎖,保證了不產生任何
髒讀、不可重複讀以及幻讀問題,但是隨之引入的是大量的讀超時以及鎖競爭,導致數據庫性能的嚴重下降。
另外來看看ANSI SQL STANDARD中,對於數據庫隔離級別以及相應問題的規定:
所以,對於REPEATABLE READ隔離級別下,是允許出現幻讀的。
- MySQL中的MVCC
MVCC(multiple-version-concurrency-control)是個行級鎖的變種,它在普通讀情況下避
免了加鎖操作,因此開銷更低。其原理具體爲,在InnoDB存儲引擎中,每行數據會加入一些隱藏字段DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE_BIT。DATA_TRX_ID 字段記錄了數據的創建和刪除時間,這個時間指的是對數據進行操作的事務的id, DATA_ROLL_PTR 指向當前數據的undo log記錄,回滾數據就是通過這個指針,DELETE BIT位用於標識該記錄是否被刪除,這裏的不是真正的刪除數據,而是標誌出來的刪除。真正意義的刪除是在mysql進行數據的GC,清理歷史版本數據的時候。
相應的,其DML的處理方式也發生了變化:
SELECT語句先查找DATA_TRX_ID早於當前事務ID的數據行。這樣就保證了讀取的數據要麼是在這個事務開始之前就已經commit了的(早於當前事務ID),要麼是在這個事務中自身創建的數據(等於當前事務ID)。查找行的DELETE_BIT爲1時,查找刪除事務ID對應的事務,確定此條記錄在當前事務開始之前,行沒有被刪除。
INSERT語句會在新插入行數據之後,保存當前事務ID作爲行的DATA_TRX_ID。
DELETE語句爲每一條刪除的記錄保存當前的事務ID作爲行的刪除標記。
UPDATE語句將複製變更的記錄,並把新記錄的DATA_TRX_ID置爲當前事務ID,同時更新老記錄中的DB_ROLL_PT指向了上一個版本。
所以在併發讀的時候,不需要等到訪問行上的鎖釋放,只需要讀取一個行的快照即可。既然是多版本的讀取,就肯定讀取不到其他事務中的新插入的數據了,也就避免了上述場景中提到的幻讀。
- 幻讀
從上述信息我們已經知道,在REPEATABLE READ級別下,InnoDB採取多版本策略成功
避免了部分幻讀現象,但是實際使用中,還是會有幻讀產生,先看場景:
通過MVCC,在事務中的多次讀取不會出現幻讀,但是此時的插入操作依舊會發生主鍵重複的錯誤,並且因爲MVCC機制,在上圖中的會話1無論讀取多少次都不會讀到導致衝突產生的數據,確實就如“幻影”一般詭異。
爲了解決上述場景中的幻讀,需要簡單提一下InnoDB的行鎖機制,在InnoDB引擎下存在三種行鎖,分別爲:
-
- Record Lock:在單行記錄上的鎖
- Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身,。GAP鎖的目的,是爲了防止同一事務的兩次讀出現幻讀的情況
- Next-Key Lock: 前兩個鎖的共同使用,即鎖定了記錄本身,也鎖定了一定的範圍。
通常情況下,INSERT/UPDATE/DELETE默認會在操作的記錄上加上Next-Key Lock,而
普通的SELECT因爲MVCC的關係反而只需要讀取快照即可,所以如果業務需要再REPEATABLE READ場景下保證絕對不產生幻讀,需要手動給SELECT加鎖,在類似SELECT…WHERE加入FOR UPDATE(排它鎖)或者LOCK IN SHARE MODE(共享鎖)。
- 總結
- InnoDB中使用索引作爲檢索條件修改數據時採用行鎖,否則使用表鎖
- InnoDB自動給修改操作加鎖,給查詢操作不自動加鎖
- 在REPEATABLE READ級別下,如果要完全杜絕幻讀,需要手動給關鍵查詢語句加鎖
- 表的大部分數據需要修改時,行鎖反而不如表鎖更有效率
最後,本文只是就網易雲信業務中數據庫的一些使用場景和問題作了總結,數據庫的實
際使用還存在諸多學問,希望能拋磚引玉,讓更多人分享出自己的心得。
邀請好友使用網易雲信,好友下單成功即可獲得500元網易考拉/嚴選無門檻現金券,點擊立即推薦>>
網易雲信(NeteaseYunXin)是集網易19年IM以及音視頻技術打造的PaaS服務產品,來自網易核心技術架構的通信與視頻雲服務,穩定易用且功能全面,致力於提供全球領先的技術能力和場景化解決方案。開發者通過集成客戶端SDK和雲端OPEN API,即可快速實現包含IM、音視頻通話、直播、點播、互動白板、短信等功能。