漫談MySQL的鎖機制

1 MySQL的三種鎖

1.1 表鎖

  • 開銷小,加鎖快
  • 不會出現死鎖
  • 鎖定粒度大,發生鎖衝突的機率最高,併發度最低

1.2 行鎖

  • 開銷大,加鎖慢
  • 會出現死鎖
  • 鎖定粒度小,發生鎖衝突的機率最低,併發度最高

1.3 頁鎖

  • 開銷和加鎖時間介於表鎖和行鎖之間
  • 會出現死鎖
  • 鎖定粒度介於表鎖和行鎖之間,併發度通常

1.4 引擎與鎖

  • MyISAM和MEMORY支持表鎖
  • BDB支持頁鎖,也支持表鎖
  • Innodb既支持行鎖,也支持表鎖,默認行鎖

1.5 查詢表鎖爭用狀況

檢查table_locks_waitedtable_locks_immediate狀態變量分析html

  • table_locks_immediate : 能夠當即獲取鎖的次數
  • table_locks_waited : 不能當即獲取鎖,須要等待鎖的次數

image

image

table_locks_waited 的值越高,則說明存在嚴重的表級鎖的爭用狀況數據庫

2 表鎖模式(MyISAM)

MySQL的表鎖有兩種模式服務器

  • 表共享讀鎖(Table Read Lock)
  • 表獨佔寫鎖(Table Write Lock)

2.1 表鎖兼容性

鎖模式的兼容以下表session

是否兼容 請求none 請求讀鎖 請求寫鎖
當前處於讀鎖
當前處於寫鎖

可見,對MyISAM表的讀操做,不會阻塞其餘用戶對同一表的讀請求,但會阻塞對同一表的寫請求;數據結構

對MyISAM表的寫操做,則會阻塞其餘用戶對同一表的讀和寫請求;併發

MyISAM表的讀和寫操做之間,以及寫和寫操做之間是串行的!(當某一線程得到對一個表的寫鎖後,只有持有鎖的線程能夠對錶進行更新操做.其餘線程的讀、寫操做都會等待,直到鎖被釋放爲止)性能

2.2 如何加表鎖

對於 MyISAM 引擎優化

  • 執行select前,會自動給涉及的全部表加
  • 執行更新(update,delete,insert)會自動給涉及到的表加

不須要用戶直接顯式用lock table命令spa

對於給MyISAM顯式加鎖,通常是爲了在必定程度上模擬事務操做,實現對某一個時間點多個表一致性讀取線程

2.2.1 實例

  • 訂單表 - orders
    記錄各訂單的總金額total
  • 訂單明細表 - order_detail
    記錄各訂單每一產品的金額小計subtotal

假設咱們須要檢查這兩個表的金額合計是否相符,可能就須要執行以下兩條SQL

圖片上傳失敗...(image-3017e3-1547370332969)

這時,若是不先給這兩個表加鎖,就可能產生錯誤的結果;

由於第一條語句執行過程當中,order_detail表可能已經發生了改變.

所以,正確寫法應該以下

圖片上傳失敗...(image-8081d7-1547370332969)

2.2.2 注意點

  • 上面的例子在LOCK TABLES時加了‘local’選項,其做用就是在知足MyISAM表併發插入條件的狀況下,容許其餘用戶在表尾插入記錄
  • 在用LOCK TABLES給表顯式加表鎖時,必須同時取得全部涉及表的鎖,而且MySQL支持鎖升級;
    也就是說,在執行LOCK TABLES後,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;
    同時,若是加的是讀鎖,那麼只能執行查詢操做,而不能執行更新操做
    其實,在自動加鎖的狀況下也基本如此,MySQL會一次得到SQL語句所須要的所有鎖.這也正是MyISAM表不會出現死鎖(Deadlock Free)的緣由
session1 session2
得到表 film_text 的讀鎖 lock table film_text read;
可select * from film_text 可select * from film_text
不能查詢沒有鎖定的表 :select * from film 可查詢/更新未鎖定的表: select * from film
插入或更新鎖定表會提示錯誤 update...from film_text 更新鎖定表會等待 update...from film_text
釋放鎖 unlock tables 等待
得到鎖,更新成功

2.3 tips

當使用lock tables時,不只須要一次鎖定用到的全部表

且同一表在SQL語句中出現多少次,就要經過與SQL語句中別名鎖多少次

lock table actor read複製代碼

會提示錯誤

select a.first_name.....複製代碼

須要對別名分別鎖定

lock table actor as a read,actor as b read;複製代碼

3 MyISAM的併發鎖

在必定條件下,MyISAM也支持併發插入和讀取

3.1 系統變量 : concurrent_insert

控制其併發插入的行爲,其值分別能夠爲

  • 0 不容許併發插入,全部插入對錶加互斥鎖
  • 1 只要表中無空洞,就容許併發插入.
    MyISAM容許在一個讀表的同時,另外一個進程從表尾插入記錄(MySQL的默認設置)
  • 2 不管MyISAM表中有無空洞,都強制在表尾併發插入記錄
    若無讀線程,新行插入空洞中

能夠利用MyISAM的併發插入特性,來解決應用中對同表查詢和插入的鎖爭用

例如,將concurrent_insert系統變量設爲2,老是容許併發插入;

同時,經過按期在系統空閒時段執行OPTIONMIZE TABLE語句來整理空間碎片,收到因刪除記錄而產生的中間空洞

刪除操做不會重整整個表,只是把 行 標記爲刪除,在表中留下空洞

MyISAM傾向於在可能時填滿這些空洞,插入時就會重用這些空間,無空洞則把新行插到表尾

3.2 MyISAM的鎖調度

MyISAM的讀和寫鎖互斥,讀操做串行的

  • 一個進程請求某個MyISAM表的讀鎖,同時另外一個進程也請求同表的寫鎖,MySQL如何處理呢?
    寫進程先得到鎖!!!
    不只如此,即便讀進程先請求先到鎖等待隊列,寫請求後到,寫鎖也會插到讀請求以前!!!

這是由於MySQL認爲寫請求通常比讀請求重要

這也正是MyISAM不適合有大量更新 / 查詢操做應用的緣由

大量的更新操做會形成查詢操做很難得到讀鎖,從而可能永遠阻塞

幸虧,咱們能夠經過一些設置來調節MyISAM的調度行爲

  • 指定啓動參數low-priority-updates
    使MyISAM引擎默認給予讀請求以優先權利
  • 執行命令SET LOW_PRIORITY_UPDATES=1
    使該鏈接發出的更新請求優先級下降
  • 指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性
    下降該語句的優先級

雖然上面3種方法都是要麼更新優先,要麼查詢優先,但仍是能夠用其來解決查詢相對重要的應用(如用戶登陸系統)中,讀鎖等待嚴重的問題

另外,MySQL也提供了一種折中的辦法來調節讀寫衝突;

即給系統參數max_write_lock_count設置一個合適的值;

當一個表的讀鎖達到這個值後,MySQL便暫時將寫請求的優先級下降,給讀進程必定得到鎖的機會

* * *

4 InnoDB鎖

InnoDB與MyISAM的最大不一樣有兩點

  • 支持事務
  • 採用行鎖

行級鎖和表級鎖原本就有許多不一樣之處,另外,事務的引入也帶來了一些新問題

4.1 事務

一組SQL語句組成的邏輯處理單元

  • 原子性(Actomicity)
    事務是一個原子操做單元,其對數據的修改,要麼全都執行,要麼全都不執行
  • 一致性(Consistent)
    在事務開始和完成時,數據都必須保持一致狀態
    這意味着全部相關的數據規則都必須應用於事務的修改,以保持完整性
    事務結束時,全部的內部數據結構(如B樹索引或雙向鏈表)也都必須是正確的
  • 隔離性(Isolation)
    一個事務所作的修改在最終提交前對其餘事務不可見
  • 持久性(Durability)
    一旦事務提交,它對於數據的修改會持久化到DB

4.2 事務的問題

相對於串行處理來講,併發事務處理能大大增長數據庫資源的利用率,提升數據庫系統的事務吞吐量,從而能夠支持能夠支持更多的用戶

但併發事務處理也會帶來一些問題,主要包括如下幾種狀況

  • 更新丟失(Lost Update)
    當多個事務選擇同一行,而後基於最初選定值更新該行時,因爲事務隔離性,最後的更新覆蓋了其餘事務所作的更新.
    例如,兩個編輯人員製做了同一文檔的電子副本。每一個編輯人員獨立地更改其副本,而後保存更改後的副本,這樣就覆蓋了原始文檔。最後保存其更改保存其更改副本的編輯人員覆蓋另外一個編輯人員所作的修改;
    若是在一個編輯人員完成並提交事務以前,另外一個編輯人員不能訪問同一文件,則可避免此問題
  • 髒讀(Dirty Reads)
    一個事務正在對一條記錄作修改,在該事務提交前,這條記錄的數據就處於不一致狀態
    這時,另外一個事務也來讀取同一條記錄,讀取了這些未提交的數據
  • 不可重複讀(Non-Repeatable Reads)
    一個事務在讀取某些數據已經發生了改變、或某些記錄已經被刪除
  • 幻讀(Phantom Reads)
    一個事務按相同的查詢條件從新讀取之前檢索過的數據,卻發現其餘事務插入了知足其查詢條件的新數據

4.3 事務隔離級別

在併發事務的問題中,「更新丟失」一般應該是徹底避免的;

但防止更新丟失,並不能單靠數據庫事務控制器來解決,須要應用程序對要更新的數據加必要的鎖來解決,所以,防止更新丟失應該是應用的責任

「髒讀」、「不可重複讀」和「幻讀」,其實都是數據庫讀一致性問題,必須由數據庫提供必定的事務隔離機制來解決

數據庫實現事務隔離的方式,基本能夠分爲如下兩種

  • 在讀取數據前,對其加鎖,防止其餘事務對數據進行修改
  • 不加任何鎖,經過必定機制生成一個數據請求時間點的一致性數據快照,並用這個快照來提供必定級別(語句級或事務級)的一致性讀取.
    從用戶的角度,好像是數據庫能夠提供同一數據的多個版本,所以,這種技術叫作數據多版本併發控制(MultiVersion Concurrency Control,MVCC),也常常稱爲多版本數據庫

數據庫的事務隔離級別越嚴格,併發反作用越小,但付出的代價也越大

由於事務隔離實質上就是使事務在必定程度上「串行化」進行,這顯然與「併發」矛盾

爲了解決「隔離」與「併發」的矛盾,ANSI SQL定義了4種隔離級別

隔離級別/讀數據一致性及容許的併發反作用 讀數據一致性 髒讀 不可重複讀 幻讀
未提交讀(Read uncommitted) 最低級別,只能保證不讀取物理上損壞的數據
已提交度(Read committed) 語句級
可重複讀(Repeatable read) 事務級
可序列化(Serializable) 最高級別,事務級

查看Innodb行鎖爭用狀況

若是發現爭用比較嚴重,如Innodb_row_lock_waitsInnodb_row_lock_time_avg的值比較高

查詢information_schema相關表來查看鎖狀況

設置Innodb monitors

進一步觀察發生鎖衝突的表,數據行等,並分析鎖爭用的緣由

中止監視器

drop table innodb_monitor

默認狀況每15秒會向日志中記錄監控的內容;

若是長時間打開會致使.err文件變得很是巨大;

因此確認緣由後,要刪除監控表關閉監視器,或者經過使用--console選項來啓動服務器以關閉寫日誌功能

4.4 InnoDB的行鎖

InnoDB支持如下兩種類型的行鎖

  • 共享鎖(讀鎖S)
    若事務 T 對數據對象 A 加了 S 鎖;
    則事務 T 能夠讀 A 但不能修改 A;
    其它事務只能再對它加 S 鎖,而不能加 X 鎖,直到 T 釋放 A 上的 S 鎖;
    這保證了,其餘事務能夠讀 A,但在事務 T 釋放 S 鎖以前,不能對 A 作任何修改操做.
  • 排他鎖(寫鎖X)
    若事務 T 對數據對象A加 X 鎖;
    事務 T 能夠讀 A 也能夠修改 A;
    其餘事務不能對 A 加任何鎖,直到 T 釋放 A 上的鎖;
    這保證了,其餘事務在 T 釋放 A 上的鎖以前不能再讀取和修改 A .

MySQL InnoDB默認行級鎖

行級鎖都是基於索引的,若一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住

爲了容許行/表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks)

這兩種意向鎖都是表鎖

  • 意向共享鎖(IS)
    事務打算給數據行共享鎖;
    事務在給一個數據行加共享鎖前必須先取得該表的IS鎖
  • 意向排他鎖(IX)
    事務打算給數據行加排他鎖;
    事務在給一個數據行加排他鎖前必須先取得該表的IX鎖
當前鎖/是否兼容/請求鎖 X IX S IS
X 衝突 衝突 衝突 衝突
IX 衝突 兼容 衝突 兼容
S 衝突 衝突 兼容 兼容
IS 衝突 兼容 兼容 兼容

若是一個事務請求的鎖模式與當前鎖兼容,InnoDB就請求的鎖授予該事務;

反之,若是二者二者不兼容,該事務就要等待鎖釋放

意向鎖是InnoDB自動加的,不需用戶干預.

對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及及數據集加排他鎖(X);

對於普通SELECT語句,InnoDB不會加任何鎖.

對於SELECT語句,能夠經過如下語句顯式地給記錄加讀/寫鎖

  • 共享鎖(S)
  • 排他鎖(X)

共享鎖語句主要用在須要數據依存關係時確認某行記錄是否存在;

並確保沒有人對這個記錄UPDATE或DELETE.

但若是當前事務也須要對該記錄進行更新,則頗有可能形成死鎖;

對於鎖定行記錄後須要進行更新操做的應用,應該使用排他鎖語句.

4.5 實例

4.5.1 Innodb共享鎖

session_1 session_2
set autocommit=0,select * from actor where id =1 set autocommit=0,select * from actor where id =1
當前seesion對id爲1的記錄加入共享鎖 select * from actor where id =1 lock in share mode
其餘seesion仍然能夠查詢,並對該記錄加入 select * from actor where id =1 lock in share mode
當前session對鎖定的記錄進行更新,等待鎖 update。。。where id=1
當前session對鎖定記錄進行更新,則會致使死鎖退出 update。。。where id=1

| 得到鎖,更新成功 |

4.5.2 Innodb排他鎖

session_1 session_2
set autocommit=0,select * from actor where id =1 set autocommit=0,select * from actor where id =1
當前seesion對id爲1的記錄加入for update 共享鎖 select * from actor where id =1 for update
可查詢該記錄select from actor where id =1,可是不能再記錄共享鎖,會等待得到鎖select from actor where id =1 for update
更新後釋放鎖 update。。。 commit
其餘session,得到鎖,獲得其餘seesion提交的記錄

4.6 行鎖的實現

行鎖是經過給索引上的索引項加鎖來實現

若是沒有索引,InnoDB將經過隱藏的聚簇索引來對記錄加鎖

  • Record Locks:對索引項加鎖
  • Gap lock:對索引項之的「間隙「,第一條記錄前的」間隙「,或最後一條記錄後的」間隙「,加鎖
  • Next-key lock:前兩種的組合,對記錄及其前面的間隙加鎖

行鎖實現特色意味着:

若是不經過索引條件檢索數據,那麼Innodb將對錶的全部記錄加鎖,和表鎖同樣

間隙鎖(Next-Key鎖)

當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據的索引項加鎖;

對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖).

舉例來講,假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101,下面的SQL:

InnoDB 不只會對符合條件的 empid 值爲 101 的記錄加鎖;

也會對 empid大於101(這些記錄並不存在)的「間隙」加鎖

間隙鎖的目的

  • 防止幻讀,以知足相關隔離級別的要求
    對於上例,若不使用間隙鎖,若是其餘事務插入 empid 大於 100 的任何記錄,;
    那麼本事務若是再次執行上述語句,就會發生幻讀
  • 知足其恢復和複製的須要
    在使用範圍條件檢索並鎖定記錄時;
    InnoDB 這種加鎖機制會阻塞符合條件範圍內鍵值的併發插入,這每每會形成嚴重的鎖等待;
    所以,在實際開發中,尤爲是併發插入較多的應用;
    咱們要儘可能優化業務邏輯,儘可能使用相等條件來訪問更新數據,避免使用範圍條件.

4.7 when 使用表鎖

對於InnoDB,在絕大部分狀況下都應該使用行鎖

由於事務,行鎖每每是咱們選擇InnoDB的理由

但在個別特殊事務中,也能夠考慮使用表鎖

  • 事務須要更新大部分數據,表又較大
    若使用默認的行鎖,不只該事務執行效率低(由於須要對較多行加鎖,加鎖是須要耗時的);
    並且可能形成其餘事務長時間鎖等待和鎖衝突;
    這種狀況下能夠考慮使用表鎖來提升該事務的執行速度
  • 事務涉及多個表,較複雜,極可能引發死鎖,形成大量事務回滾
    這種狀況也能夠考慮一次性鎖定事務涉及的表,從而避免死鎖、減小數據庫因事務回滾帶來的開銷

固然,應用中這兩種事務不能太多,不然,就應該考慮使用MyISAM

在InnoDB下 ,使用表鎖要注意

  • 使用LOCK TALBES雖然能夠給InnoDB加表鎖
    表鎖不是由InnoDB引擎層管理的,而是由其上一層MySQL Server負責;
    僅當autocommit=0、innodb_table_lock=1(默認設置),InnoDB 引擎層才知道MySQL加的表鎖,MySQL Server才能感知InnoDB加的行鎖;
    這種狀況下,InnoDB才能自動識別涉及表鎖的死鎖
    不然,InnoDB將沒法自動檢測並處理這種死鎖
  • 在用LOCK TALBESInnoDB鎖時要注意,要將autocommit設爲0,不然MySQL不會給表加鎖
    事務結束前,不要用UNLOCK TALBES釋放表鎖,由於它會隱式地提交事務
    COMMIT或ROLLBACK不能釋放用LOCK TALBES加的表鎖,必須用UNLOCK TABLES釋放表鎖,正確的方式見以下語句
  • 須要寫表t1並從表t讀

5 死鎖

MyISAM表鎖是deadlock free的,這是由於MyISAM老是一次性得到所需的所有鎖,要麼所有知足,要麼等待,所以不會出現死鎖

但在InnoDB中,除單個SQL組成的事務外,鎖是逐步得到的,這就決定了InnoDB發生死鎖是可能的

發生死鎖後,InnoDB通常都能自動檢測到,並使一個事務釋放鎖並退回,另外一個事務得到鎖,繼續完成事務

  • 但在涉及外部鎖,或涉及鎖的狀況下,InnoDB並不能徹底自動檢測到死鎖
    這須要經過設置鎖等待超時參數innodb_lock_wait_timeout來解決
    須要說明的是,這個參數並非只用來解決死鎖問題,在併發訪問比較高的狀況下,若是大量事務因沒法當即獲取所需的鎖而掛起,會佔用大量計算機資源,形成嚴重性能問題,甚至拖垮數據庫
    咱們經過設置合適的鎖等待超時閾值,能夠避免這種狀況發生。

一般來講,死鎖都是應用設計的問題,經過調整業務流程、數據庫對象設計、事務大小、以及訪問數據庫的SQL語句,絕大部分均可以免

下面就經過實例來介紹幾種死鎖的經常使用方法。

  • 應用中,不一樣的程序會併發存取多個表
    儘可能約定以相同的順序訪問表
  • 程序批處理數據時
    事先對數據排序,保證每一個線程按固定的順序來處理記錄
  • 在事務中,要更新記錄
    直接申請排他鎖,而不該該先申請共享鎖
  • 可重複讀下,若是兩個線程同時對相同條件記錄用SELECT...ROR UPDATE加排他寫鎖
    在沒有符合該記錄狀況下,兩個線程都會加鎖成功
    程序發現記錄尚不存在,就試圖插入一條新記錄,若是兩個線程都這麼作,就會出現死鎖
    這種狀況下,將隔離級別改爲READ COMMITTED,就能夠避免問題
  • 當隔離級別爲READ COMMITED時,若是兩個線程都先執行SELECT...FOR UPDATE
    判斷是否存在符合條件的記錄,沒有 -> 插入記錄;
    此時,只有一個線程能插入成功,另外一個線程會出現鎖等待.
    當第1個線程提交後,第2個線程會因主鍵重出錯,但雖然這個線程出錯了,卻會得到一個排他鎖!這時若是有第3個線程又來申請排他鎖,也會出現死鎖.
    對於這種狀況,能夠直接作插入操做,而後再捕獲主鍵重異常,或者在遇到主鍵重錯誤時,老是執行ROLLBACK釋放得到的排他鎖

若是出現死鎖,能夠用SHOW INNODB STATUS命令來肯定最後一個死鎖產生的緣由和改進措施。

6 總結

6.1 MyISAM的表鎖

  • 共享讀鎖之間是兼容的,但共享讀鎖和排他寫鎖之間,以及排他寫鎖之間互斥,即讀寫串行
  • 在必定條件下,MyISAM容許查詢/插入併發,可利用這一點來解決應用中對同一表查詢/插入的鎖爭用問題
  • MyISAM默認的鎖調度機制是寫優先,這並不必定適合全部應用,用戶能夠經過設置LOW_PRIPORITY_UPDATES參數或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖的爭用
  • 因爲表鎖的鎖定粒度大,讀寫又是串行的,所以若是更新操做較多,MyISAM表可能會出現嚴重的鎖等待,能夠考慮採用InnoDB表來減小鎖衝突

6.2 對於InnoDB表

  • 行鎖基於索引實現
    若是不經過索引訪問數據,InnoDB會使用表鎖
  • 間隙鎖機制及使用間隙鎖的緣由
  • 不一樣的隔離級別下,InnoDB的鎖機制和一致性讀策略不一樣
  • MySQL的恢復和複製對InnoDB鎖機制和一致性讀策略也有較大影響
  • 鎖衝突甚至死鎖很難徹底避免

7 索引與鎖

在瞭解InnoDB的鎖特性後,用戶能夠經過設計和SQL調整等措施減小鎖衝突和死鎖

  • 儘可能使用較低的隔離級別
  • 精心設計索引,並儘可能使用索引訪問數據,使加鎖更精確,從而減小鎖衝突的機會。
  • 選擇合理的事務大小,小事務發生鎖衝突的概率也更小
  • 給記錄集顯式加鎖時,最好一次性請求足夠級別的鎖。好比要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖。
  • 不一樣的程序訪問一組表時,應儘可能約定以相同的順序訪問各表,對一個表而言,儘量以固定的順序存取表中的行。這樣能夠大減小死鎖的機會。
  • 儘可能用相等條件訪問數據,這樣能夠避免間隙鎖對併發插入的影響。
  • 不要申請超過實際須要的鎖級別;除非必須,查詢時不要顯示加鎖。
  • 對於一些特定的事務,可使用表鎖來提升處理速度或減小死鎖的可能

    image
    image
    image
    image

參考

MySQL中的鎖(表鎖、行鎖)

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索