全面瞭解mysql鎖機制(InnoDB)與問題排查

著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。

MySQL/InnoDB的加鎖,一直是一個常見的話題。例如,數據庫若是有高併發請求,如何保證數據完整性?產生死鎖問題如何排查並解決?下面是不一樣鎖等級的區別java

  • 表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的機率最高 ,併發度最低。
  • 頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度通常。
  • 行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的機率最低,併發度也最高。

查看數據庫擁有的存儲引擎類型
SHOW ENGINESmysql

樂觀鎖

用數據版本(Version)記錄機制實現,這是樂觀鎖最經常使用的一種實現方式。何謂數據版本?即爲數據增長一個版本標識,通常是經過爲數據庫表增長一個數字類型的 「version」 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加1。當咱們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,若是數據庫表當前版本號與第一次取出來的version值相等,則予以更新,不然認爲是過時數據。git

舉例:github

一、數據庫表三個字段,分別是id、value、version
select id,value,version from TABLE where id = #{id}
二、每次更新表中的value字段時,爲了防止發生衝突,須要這樣操做sql

update TABLE
set value=2,version=version+1
where id=#{id} and version=#{version}
複製代碼

悲觀鎖

與樂觀鎖相對應的就是悲觀鎖了。悲觀鎖就是在操做數據時,認爲此操做會出現數據衝突,因此在進行每次操做時都要經過獲取鎖才能進行對相同數據的操做,這點跟java中的synchronized很類似,因此悲觀鎖須要耗費較多的時間。另外與樂觀鎖相對應的,悲觀鎖是由數據庫本身實現了的,要用的時候,咱們直接調用數據庫的相關語句就能夠了。數據庫

說到這裏,由悲觀鎖涉及到的另外兩個鎖概念就出來了,它們就是共享鎖與排它鎖。共享鎖和排它鎖是悲觀鎖的不一樣的實現,它倆都屬於悲觀鎖的範疇。segmentfault

共享鎖

共享鎖又稱讀鎖 (read lock),是讀取操做建立的鎖。其餘用戶能夠併發讀取數據,但任何事務都不能對數據進行修改(獲取數據上的排他鎖),直到已釋放全部共享鎖。當若是事務對讀鎖進行修改操做,極可能會形成死鎖。以下圖所示。bash

若是事務T對數據A加上共享鎖後,則其餘事務只能對A再加共享鎖,不能加排他鎖。得到共享鎖的事務只能讀數據,不能修改數據session

打開第一個查詢窗口併發

begin;/begin work;/start transaction;  (三者選一就能夠)
#(lock in share mode 共享鎖)
SELECT * from TABLE where id = 1  lock in share mode;
複製代碼

而後在另外一個查詢窗口中,對id爲1的數據進行更新
update TABLE set name="www.souyunku.com" where id =1;
此時,操做界面進入了卡頓狀態,過了好久超時,提示錯誤信息
若是在超時前,第一個窗口執行commit,此更新語句就會成功。

[SQL]update test_one set name="www.souyunku.com" where id =1;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
複製代碼

加上共享鎖後,也提示錯誤信息

update test_one set name="www.souyunku.com" where id =1 lock in share mode;
[SQL]update  test_one set name="www.souyunku.com" where id =1 lock in share mode;
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1
複製代碼

在查詢語句後面增長 LOCK IN SHARE MODE ,Mysql會對查詢結果中的每行都加共享鎖,當沒有其餘線程對查詢結果集中的任何一行使用排他鎖時,能夠成功申請共享鎖,不然會被阻塞。 其餘線程也能夠讀取使用了共享鎖的表,並且這些線程讀取的是同一個版本的數據。

加上共享鎖後,對於update,insert,delete語句會自動加排它鎖。

排它鎖

排他鎖 exclusive lock(也叫writer lock)又稱寫鎖。
名詞解釋:若某個事物對某一行加上了排他鎖,只能這個事務對其進行讀寫,在此事務結束以前,其餘事務不能對其進行加任何鎖,其餘進程能夠讀取,不能進行寫操做,需等待其釋放。 排它鎖是悲觀鎖的一種實現,在上面悲觀鎖也介紹過。

若事務 1 對數據對象A加上X鎖,事務 1 能夠讀A也能夠修改A,其餘事務不能再對A加任何鎖,直到事物 1 釋放A上的鎖。這保證了其餘事務在事物 1 釋放A上的鎖以前不能再讀取和修改A。排它鎖會阻塞全部的排它鎖和共享鎖

讀取爲何要加讀鎖呢?防止數據在被讀取的時候被別的線程加上寫鎖。 排他鎖使用方式:在須要執行的語句後面加上for update就能夠了 select status from TABLE where id=1 for update;

排他鎖,也稱寫鎖,獨佔鎖,當前寫操做沒有完成前,它會阻斷其餘寫鎖和讀鎖。

排它鎖-舉例:

要使用排他鎖,咱們必須關閉mysql數據庫的自動提交屬性,由於MySQL默認使用autocommit模式,也就是說,當你執行一個更新操做後,MySQL會馬上將結果進行提交。

咱們可使用命令設置MySQL爲非autocommit模式:

set autocommit=0;
# 設置完autocommit後,咱們就能夠執行咱們的正常業務了。具體以下:
# 1. 開始事務
begin;/begin work;/start transaction; (三者選一就能夠)
# 2. 查詢表信息(for update加鎖)
select status from TABLE where id=1 for update;
# 3. 插入一條數據
insert into TABLE (id,value) values (2,2);
# 4. 修改數據爲
update TABLE set value=2 where id=1;
# 5. 提交事務
commit;/commit work
複製代碼

行鎖

總結:多個事務操做同一行數據時,後來的事務處於阻塞等待狀態。這樣能夠避免了髒讀等數據一致性的問題。後來的事務能夠操做其餘行數據,解決了表鎖高併發性能低的問題。

# Transaction-A
mysql> set autocommit = 0;
mysql> update innodb_lock set v='1001' where id=1;
mysql> commit;

# Transaction-B
mysql> update innodb_lock set v='2001' where id=2;
Query OK, 1 row affected (0.37 sec)
mysql> update innodb_lock set v='1002' where id=1;
Query OK, 1 row affected (37.51 sec)
複製代碼

現實:當執行批量修改數據腳本的時候,行鎖升級爲表鎖。其餘對訂單的操做都處於等待中,,, 緣由:nnoDB只有在經過索引條件檢索數據時使用行級鎖,不然使用表鎖! 而模擬操做正是經過id去做爲檢索條件,而id又是MySQL自動建立的惟一索引,因此才忽略了行鎖變表鎖的狀況

總結:InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖。而且該索引不能失效,不然都會從行鎖升級爲表鎖。

  • 行鎖的劣勢:開銷大;加鎖慢;會出現死鎖
  • 行鎖的優點:鎖的粒度小,發生鎖衝突的機率低;處理併發的能力強
  • 加鎖的方式:自動加鎖。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖;對於普通SELECT語句,InnoDB不會加任何鎖;固然咱們也能夠顯示的加鎖:

從上面的案例看出,行鎖變表鎖彷佛是一個坑,可MySQL沒有這麼無聊給你挖坑。這是由於MySQL有本身的執行計劃。 當你須要更新一張較大表的大部分甚至全表的數據時。而你又傻乎乎地用索引做爲檢索條件。一不當心開啓了行鎖(沒毛病啊!保證數據的一致性!)。可MySQL卻認爲大量對一張表使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突問題,性能嚴重降低。因此MySQL會將行鎖升級爲表鎖,即實際上並無使用索引。 咱們仔細想一想也能理解,既然整張表的大部分數據都要更新數據,在一行一行地加鎖效率則更低。其實咱們能夠經過explain命令查看MySQL的執行計劃,你會發現key爲null。代表MySQL實際上並無使用索引,行鎖升級爲表鎖也和上面的結論一致。

注意:行級鎖都是基於索引的,若是一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。

間隙鎖

當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。 舉例來講,假如emp表中只有101條記錄,其empid的值分別是 1,2,...,100,101,下面的SQL:

Select * from  emp where empid > 100 for update;
複製代碼

是一個範圍條件的檢索,InnoDB不只會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的「間隙」加鎖。

InnoDB使用間隙鎖的目的,一方面是爲了防止幻讀,以知足相關隔離級別的要求,對於上面的例子,要是不使用間隙鎖,若是其餘事務插入了empid大於100的任何記錄,那麼本事務若是再次執行上述語句,就會發生幻讀;另一方面,是爲了知足其恢復和複製的須要。有關其恢復和複製對鎖機制的影響,以及不一樣隔離級別下InnoDB使用間隙鎖的狀況,在後續的章節中會作進一步介紹。

很顯然,在使用範圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍內鍵值的併發插入,這每每會形成嚴重的鎖等待。所以,在實際應用開發中,尤爲是併發插入比較多的應用,咱們要儘可能優化業務邏輯,儘可能使用相等條件來訪問更新數據,避免使用範圍條件。

還要特別說明的是,InnoDB除了經過範圍條件加鎖時使用間隙鎖外,若是使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!

例子:假如emp表中只有101條記錄,其empid的值分別是1,2,......,100,101。
InnoDB存儲引擎的間隙鎖阻塞例子

session_1 session_2
mysql> select @@tx_isolation; mysql> select @@tx_isolation;
+-----------------+ +-----------------+
@@tx_isolation @@tx_isolation
+-----------------+ +-----------------+
REPEATABLE-READ REPEATABLE-READ
+-----------------+ +-----------------+
1 row in set (0.00 sec) 1 row in set (0.00 sec)
mysql> set autocommit = 0; mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec) Query OK, 0 rows affected (0.00 sec)
當前session對不存在的記錄加for update的鎖:
mysql> select * from emp where empid = 102 for update;
Empty set (0.00 sec)
這時,若是其餘session插入empid爲201的記錄(注意:這條記錄並不存在),也會出現鎖等待:
mysql>insert into emp(empid,...) values(201,...);
阻塞等待
Session_1 執行rollback:
mysql> rollback;
Query OK, 0 rows affected (13.04 sec)
因爲其餘session_1回退後釋放了Next-Key鎖,當前session能夠得到鎖併成功插入記錄:
mysql>insert into emp(empid,...) values(201,...);
Query OK, 1 row affected (13.35 sec)

危害(坑):若執行的條件是範圍過大,則InnoDB會將整個範圍內全部的索引鍵值所有鎖定,很容易對性能形成影響。

表鎖

如何加表鎖? innodb 的行鎖是在有索引的狀況下,沒有索引的表是鎖定全表的。

Innodb中的行鎖與表鎖

前面提到過,在Innodb引擎中既支持行鎖也支持表鎖,那麼何時會鎖住整張表,何時只鎖住一行呢? 只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖!

在實際應用中,要特別注意InnoDB行鎖的這一特性,否則的話,可能致使大量的鎖衝突,從而影響併發性能。

行級鎖都是基於索引的,若是一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。行級鎖的缺點是:因爲須要請求大量的鎖資源,因此速度慢,內存消耗大。

死鎖

死鎖(Deadlock) 所謂死鎖:是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。因爲資源佔用是互斥的,當某個進程提出申請資源後,使得有關進程在無外力協助下,永遠分配不到必需的資源而沒法繼續運行,這就產生了一種特殊現象死鎖。

解除正在死鎖的狀態有兩種方法:
第一種:

  1. 查詢是否鎖表 show OPEN TABLES where In_use > 0;
  2. 查詢進程(若是您有SUPER權限,您能夠看到全部線程。不然,您只能看到您本身的線程)
    show processlist
  3. 殺死進程id(就是上面命令的id列)
    kill id

第二種:

  1. 查看當前的事務
    SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
  2. 查看當前鎖定的事務
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
  3. 查看當前等鎖的事務
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
    殺死進程
    kill 進程ID

若是系統資源充足,進程的資源請求都可以獲得知足,死鎖出現的可能性就很低,不然就會因爭奪有限的資源而陷入死鎖。其次,進程運行推動順序與速度不一樣,也可能產生死鎖。 產生死鎖的四個必要條件:

  1. 互斥條件:一個資源每次只能被一個進程使用。
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。
  3. 不剝奪條件:進程已得到的資源,在末使用完以前,不能強行剝奪。
  4. 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。

雖然不能徹底避免死鎖,但可使死鎖的數量減至最少。將死鎖減至最少能夠增長事務的吞吐量並減小系統開銷,由於只有不多的事務回滾,而回滾會取消事務執行的全部工做。因爲死鎖時回滾的操做由應用程序從新提交。

下列方法有助於最大限度地下降死鎖:

  1. 按同一順序訪問對象。
  2. 避免事務中的用戶交互。
  3. 保持事務簡短並在一個批處理中。
  4. 使用低隔離級別。
  5. 使用綁定鏈接。

MyISAM存儲引擎

InnoDB和MyISAM的最大不一樣點有兩個:

  1. InnoDB支持事務(transaction);MyISAM不支持事務
  2. Innodb 默認採用行鎖, MyISAM 是默認採用表鎖。加鎖能夠保證事務的一致性,可謂是有人(鎖)的地方,就有江湖(事務)
  3. MyISAM不適合高併發

共享讀鎖

對MyISAM表的讀操做(加讀鎖),不會阻塞其餘進程對同一表的讀操做,但會阻塞對同一表的寫操做。只有當讀鎖釋放後,才能執行其餘進程的寫操做。在鎖釋放前不能讀其餘表。

獨佔寫鎖

對MyISAM表的寫操做(加寫鎖),會阻塞其餘進程對同一表的讀和寫操做,只有當寫鎖釋放後,纔會執行其餘進程的讀寫操做。在鎖釋放前不能寫其餘表。

總結:

  1. 表鎖,讀鎖會阻塞寫,不會阻塞讀。而寫鎖則會把讀寫都阻塞。
  2. 表鎖的加鎖/解鎖方式:MyISAM 在執行查詢語句(SELECT)前,會自動給涉及的全部表加讀鎖,在執行更新操做 (UPDATE、DELETE、INSERT 等)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預,所以,用戶通常不須要直接用LOCK TABLE命令給MyISAM表顯式加鎖。

若是用戶想要顯示的加鎖可使用如下命令:

鎖定表:

LOCK TABLES tbl_name {READ | WRITE},[ tbl_name {READ | WRITE},…] 
複製代碼

解鎖表:

UNLOCK TABLES 
複製代碼

在用 LOCK TABLES 給表顯式加表鎖時,必須同時取得全部涉及到表的鎖。 在執行 LOCK TABLES 後,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;

若是加的是讀鎖,那麼只能執行查詢操做,而不能執行更新操做。

在自動加鎖的狀況下也基本如此,MyISAM 老是一次得到 SQL 語句所須要的所有鎖。這也正是 MyISAM 表不會出現死鎖(Deadlock Free)的緣由。

對錶test_table增長讀鎖:

LOCK TABLES test_table READ 
UNLOCK test_table
複製代碼

對錶test_table增長寫鎖

LOCK TABLES test_table WRITE
UNLOCK test_table
複製代碼

當使用 LOCK TABLES 時,不只須要一次鎖定用到的全部表,並且,同一個表在 SQL 語句中出現多少次,就要經過與 SQL 語句中相同的別名鎖定多少次,不然也會出錯!

好比以下SQL語句:

select a.first_name,b.first_name, from actor a,actor b where a.first_name = b.first_name;
複製代碼

該Sql語句中,actor表以別名的方式出現了兩次,分別是a,b,這時若是要在該Sql執行以前加鎖就要使用如下Sql:

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

併發插入

上文說到過 MyISAM 表的讀和寫是串行的,但這是就整體而言的。在必定條件下,MyISAM表也支持查詢和插入操做的併發進行。 MyISAM存儲引擎有一個系統變量concurrent_insert,專門用以控制其併發插入的行爲,其值分別能夠爲0、1或2。

  • 當concurrent_insert設置爲0時,不容許併發插入。
  • 當concurrent_insert設置爲1時,若是MyISAM表中沒有空洞(即表的中間沒有被刪除的 行),MyISAM容許在一個進程讀表的同時,另外一個進程從表尾插入記錄。這也是MySQL 的默認設置。
  • 當concurrent_insert設置爲2時,不管MyISAM表中有沒有空洞,都容許在表尾併發插入記錄。

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

MyISAM的鎖調度

前面講過,MyISAM 存儲引擎的讀鎖和寫鎖是互斥的,讀寫操做是串行的。那麼,一個進程請求某個 MyISAM 表的讀鎖,同時另外一個進程也請求同一表的寫鎖,MySQL 如何處理呢?

答案是寫進程先得到鎖。

不只如此,即便讀請求先到鎖等待隊列,寫請求後到,寫鎖也會插到讀鎖請求以前!這是由於 MySQL 認爲寫請求通常比讀請求要重要。這也正是 MyISAM 表不太適合於有大量更新操做和查詢操做應用的緣由,由於大量的更新操做會形成查詢操做很難得到讀鎖,從而可能永遠阻塞。這種狀況有時可能會變得很是糟糕!

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

經過指定啓動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。

  • 經過執行命令SET LOWPRIORITYUPDATES=1,使該鏈接發出的更新請求優先級下降。
  • 經過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,下降該語句的優先級。
  • 另外,MySQL也 供了一種折中的辦法來調節讀寫衝突,即給系統參數max_write_lock_count 設置一個合適的值,當一個表的讀鎖達到這個值後,MySQL就暫時將寫請求的優先級下降, 給讀進程必定得到鎖的機會。

總結

  • 數據庫中的鎖從鎖定的粒度上分能夠分爲行級鎖、頁級鎖和表級鎖。
  • MySQL的MyISAM引擎支持表級鎖。
  • 表級鎖分爲兩種:共享讀鎖、互斥寫鎖。這兩種鎖都是阻塞鎖。
  • 能夠在讀鎖上增長讀鎖,不能在讀鎖上增長寫鎖。在寫鎖上不能增長寫鎖。
  • 默認狀況下,MySql在執行查詢語句以前會加讀鎖,在執行更新語句以前會執行寫鎖。
  • 若是想要顯示的加鎖/解鎖的花可使用LOCK TABLES和UNLOCK來進行。
  • 在使用LOCK TABLES以後,在解鎖以前,不能操做未加鎖的表。
  • 在加鎖時,若是顯示的指明是要增長讀鎖,那麼在解鎖以前,只能進行讀操做,不能執行寫操做。
  • 若是一次Sql語句要操做的表以別名的方式屢次出現,那麼就要在加鎖時都指明要加鎖的表的別名。
  • MyISAM存儲引擎有一個系統變量concurrent_insert,專門用以控制其併發插入的行爲,其值分別能夠爲0、1或2。
  • 因爲讀鎖和寫鎖互斥,那麼在調度過程當中,默認狀況下,MySql會本着寫鎖優先的原則。能夠經過low-priority-updates來設置。

實踐解決

分析行鎖定

經過檢查InnoDB_row_lock 狀態變量分析系統上中行鎖的爭奪狀況

mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 0     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 0     |
+-------------------------------+-------+
複製代碼
  • innodb_row_lock_current_waits: 當前正在等待鎖定的數量
  • innodb_row_lock_time: 從系統啓動到如今鎖定總時間長度;很是重要的參數,
  • innodb_row_lock_time_avg: 每次等待所花平均時間;很是重要的參數,
  • innodb_row_lock_time_max: 從系統啓動到如今等待最常的一次所花的時間;
  • innodb_row_lock_waits: 系統啓動後到如今總共等待的次數;很是重要的參數。直接決定優化的方向和策略。

行鎖優化

  1. 儘量讓全部數據檢索都經過索引來完成,避免無索引行或索引失效致使行鎖升級爲表鎖。
  2. 儘量避免間隙鎖帶來的性能降低,減小或使用合理的檢索範圍。
  3. 儘量減小事務的粒度,好比控制事務大小,而從減小鎖定資源量和時間長度,從而減小鎖的競爭等,提供性能。
  4. 儘量低級別事務隔離,隔離級別越高,併發的處理能力越低。

表鎖優化

查看加鎖狀況 how open tables; 1表示加鎖,0表示未加鎖。

mysql> show open tables where in_use > 0;
+----------+-------------+--------+-------------+
| Database | Table       | In_use | Name_locked |
+----------+-------------+--------+-------------+
| lock     | myisam_lock |      1 |           0 |
+----------+-------------+--------+-------------+
複製代碼

分析表鎖定

能夠經過檢查table_locks_waited 和 table_locks_immediate 狀態變量分析系統上的表鎖定:show status like 'table_locks%'

mysql> show status like 'table_locks%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 104   |
| Table_locks_waited         | 0     |
+----------------------------+-------+
複製代碼
  • table_locks_immediate: 表示當即釋放表鎖數。
  • table_locks_waited: 表示須要等待的表鎖數。此值越高則說明存在着越嚴重的表級鎖爭用狀況。

此外,MyISAM的讀寫鎖調度是寫優先,這也是MyISAM不適合作寫爲主表的存儲引擎。由於寫鎖後,其餘線程不能作任何操做,大量的更新會使查詢很可貴到鎖,從而形成永久阻塞。

什麼場景下用表鎖

第一種狀況:全表更新。事務須要更新大部分或所有數據,且表又比較大。若使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突。

第二種狀況:多表查詢。事務涉及多個表,比較複雜的關聯查詢,極可能引發死鎖,形成大量事務回滾。這種狀況若能一次性鎖定事務涉及的表,從而能夠避免死鎖、減小數據庫因事務回滾帶來的開銷。

  1. InnoDB 支持表鎖和行鎖,使用索引做爲檢索條件修改數據時採用行鎖,不然採用表鎖。
  2. InnoDB 自動給修改操做加鎖,給查詢操做不自動加鎖
  3. 行鎖可能由於未使用索引而升級爲表鎖,因此除了檢查索引是否建立的同時,也須要經過explain執行計劃查詢索引是否被實際使用。
  4. 行鎖相對於表鎖來講,優點在於高併發場景下表現更突出,畢竟鎖的粒度小。
  5. 當表的大部分數據須要被修改,或者是多表複雜關聯查詢時,建議使用表鎖優於行鎖。
  6. 爲了保證數據的一致完整性,任何一個數據庫都存在鎖定機制。鎖定機制的優劣直接影響到一個數據庫的併發處理能力和性能。

mysql 5.6 在 update 和 delete 的時候,where 條件若是不存在索引字段,那麼這個事務是否會致使表鎖? 有人回答: 只有主鍵和惟一索引纔是行鎖,普通索引是表鎖。

結果發現普通索引並不必定會引起表鎖,在普通索引中,是否引起表鎖取決於普通索引的高效程度。

上文說起的「高效」是相對主鍵和惟一索引而言,也許「高效」並非一個很好的解釋,只要明白在通常狀況下,「普通索引」效率低於其餘二者便可。 屬性值重複率高

屬性值重複率

當「值重複率」低時,甚至接近主鍵或者惟一索引的效果,「普通索引」依然是行鎖;當「值重複率」高時,MySQL 不會把這個「普通索引」當作索引,即形成了一個沒有索引的 SQL,此時引起表鎖。

同 JVM 自動優化 java 代碼同樣,MySQL 也具備自動優化 SQL 的功能。低效的索引將被忽略,這也就倒逼開發者使用正確且高效的索引。

屬性值重複率高


爲了突出效果,我將「普通索引」創建在一個「值重複率」高的屬性下。以相對極端的方式,擴大對結果的影響。

我會建立一張「分數等級表」,屬性有「id」、「score(分數)」、「level(等級)」,模擬一個半自動的業務——「分數」已被自動導入,而「等級」須要手工更新。

操做步驟以下:

  1. 取消 MySQL 的 事務自動提交
  2. 建表,id自增,並給「score(分數)」建立普通索引
  3. 插入分數值,等級爲 null
  4. 開啓兩個事務 session_一、session_2,兩個事務以「score」爲條件指定不一樣值,鎖定數據
  5. session_1 和 session_2 前後更新各自事務鎖定內容的「level」
  6. 觀察數據庫對兩個事務的響應

取消 事務自動提交:

mysql> set autocommit = off;
Query OK, 0 rows affected (0.02 sec)

mysql> show variables like "autocommit";
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| autocommit               | OFF   |
+--------------------------+-------+
1 rows in set (0.01 sec)
複製代碼

建表、建立索引、插入數據:

DROP TABLE IF EXISTS `test1`;
CREATE TABLE `test1` (
`ID`  int(5) NOT NULL AUTO_INCREMENT ,
`SCORE`  int(3) NOT NULL ,
`LEVEL`  int(2) NULL DEFAULT NULL ,
PRIMARY KEY (`ID`)
)ENGINE=InnoDB DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;

ALTER TABLE `test2` ADD INDEX index_name ( `SCORE` );

INSERT INTO `test1`(`SCORE`) VALUE (100);
……
INSERT INTO `test1`(`SCORE`) VALUE (0);
複製代碼

"SCORE" 屬性的「值重複率」奇高,達到了 50%,劍走偏鋒:

mysql> select * from test1;
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  1 |   100 | NULL  |
|  2 |     0 | NULL  |
|  5 |   100 | NULL  |
|  6 |   100 | NULL  |
|  7 |   100 | NULL  |
|  8 |   100 | NULL  |
|  9 |   100 | NULL  |
| 10 |   100 | NULL  |
| 11 |   100 | NULL  |
| 12 |   100 | NULL  |
| 13 |   100 | NULL  |
| 14 |     0 | NULL  |
| 15 |     0 | NULL  |
| 16 |     0 | NULL  |
| 17 |     0 | NULL  |
| 18 |     0 | NULL  |
| 19 |     0 | NULL  |
| 20 |     0 | NULL  |
| 21 |     0 | NULL  |
| 22 |     0 | NULL  |
| 23 |     0 | NULL  |
| 24 |   100 | NULL  |
| 25 |     0 | NULL  |
| 26 |   100 | NULL  |
| 27 |     0 | NULL  |
+----+-------+-------+
25 rows in set
複製代碼

開啓兩個事務(一個窗口對應一個事務),並選定數據:

-- SESSION_1,選定 SCORE = 100 的數據
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 100 FOR UPDATE;
Query OK, 0 rows affected

+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  1 |   100 | NULL  |
|  5 |   100 | NULL  |
|  6 |   100 | NULL  |
|  7 |   100 | NULL  |
|  8 |   100 | NULL  |
|  9 |   100 | NULL  |
| 10 |   100 | NULL  |
| 11 |   100 | NULL  |
| 12 |   100 | NULL  |
| 13 |   100 | NULL  |
| 24 |   100 | NULL  |
| 26 |   100 | NULL  |
+----+-------+-------+
12 rows in set
複製代碼

再打開一個窗口:

-- SESSION_2,選定 SCORE = 0 的數據
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 0 FOR UPDATE;
Query OK, 0 rows affected

+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  2 |     0 | NULL  |
| 14 |     0 | NULL  |
| 15 |     0 | NULL  |
| 16 |     0 | NULL  |
| 17 |     0 | NULL  |
| 18 |     0 | NULL  |
| 19 |     0 | NULL  |
| 20 |     0 | NULL  |
| 21 |     0 | NULL  |
| 22 |     0 | NULL  |
| 23 |     0 | NULL  |
| 25 |     0 | NULL  |
| 27 |     0 | NULL  |
+----+-------+-------+
13 rows in set
複製代碼

session_1 窗口,更新「LEVEL」失敗:

mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;
1205 - Lock wait timeout exceeded; try restarting transaction
複製代碼

在以前的操做中,session_1 選擇了 SCORE = 100 的數據,session_2 選擇了 SCORE = 0 的數據,看似兩個事務井水不犯河水,可是在 session_1 事務中更新本身鎖定的數據失敗,只能說明在此時引起了表鎖。彆着急,剛剛走向了一個極端——索引屬性值重複性奇高,接下來走向另外一個極端。

屬性值重複率低


  仍是同一張表,將數據刪除只剩下兩條,「SCORE」 的 「值重複率」 爲 0:

mysql> delete from test1 where id > 2;
Query OK, 23 rows affected

mysql> select * from test1;
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  1 |   100 | NULL  |
|  2 |     0 | NULL  |
+----+-------+-------+
2 rows in set
複製代碼

關閉兩個事務操做窗口,從新開啓 session_1 和 session_2,並選擇各自須要的數據:

-- SESSION_1,選定 SCORE = 100 的數據
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 100 FOR UPDATE;
Query OK, 0 rows affected

+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  1 |   100 | NULL  |
+----+-------+-------+
1 row in set

-- -----------------新窗口----------------- --

-- SESSION_2,選定 SCORE = 0 的數據
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 0 FOR UPDATE;
Query OK, 0 rows affected

+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
|  2 |     0 | NULL  |
+----+-------+-------+
1 row in set
複製代碼

session_1 更新數據成功:

mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings:0
複製代碼

相同的表結構,相同的操做,兩個不一樣的結果讓人出乎意料。第一個結果讓人以爲「普通索引」引起表鎖,第二個結果推翻了前者,兩個操做中,惟一不一樣的是索引屬性的「值重複率」。根據 單一變量 證實法,能夠得出結論:當「值重複率」低時,甚至接近主鍵或者惟一索引的效果,「普通索引」依然是行鎖;當「值重複率」高時,MySQL 不會把這個「普通索引」當作索引,即形成了一個沒有索引的 SQL,此時引起表鎖。

舉個栗子:

  1. 用戶A在銀行卡有100元錢,某一刻用戶B向A轉帳50元(稱爲B操做),同時有用戶C向A轉帳50元(稱爲C操做);
  2. B操做從數據庫中讀取他此時的餘額100,計算新的餘額爲100+50=150
  3. C操做也從數據庫中讀取他此時的餘額100,計算新的餘額爲100+50=150
  4. B操做將balance=150寫入數據庫,以後C操做也將balance=150寫入數據庫
  5. 最終A的餘額變爲150

上面的例子,A同時收到兩筆50元轉帳,最後的餘額應該是200元,但卻由於併發的問題變爲了150元,緣由是B和C向A發起轉帳請求時,同時打開了兩個數據庫會話,進行了兩個事務,後一個事務拿到了前一個事務的中間狀態數據,致使更新丟失。
經常使用的解決思路有兩種:

  • 加鎖同步執行
  • update前檢查數據一致性

要注意悲觀鎖和樂觀鎖都是業務邏輯層次的定義,不一樣的設計可能會有不一樣的實現。在mysql層經常使用的悲觀鎖實現方式是加一個排他鎖。

然而實際上並非這樣,實際上加了排他鎖的數據,在釋放鎖(事務結束)以前其餘事務不能再對該數據加鎖 排他鎖之因此能阻止update,delete等操做是由於update,delete操做會自動加排他鎖, 也就是說即便加了排他鎖也沒法阻止select操做。而select XX for update 語法能夠對select操做加上排他鎖。 因此爲了防止更新丟失能夠在select時加上for update加鎖 這樣就能夠阻止其他事務的select for update (但注意沒法阻止select)

樂觀鎖example:

begin;
select balance from account where id=1;
-- 獲得balance=100;而後計算balance=100+50=150
update account set balance = 150 where id=1 and balance = 100;
commit;
複製代碼

如上,若是sql在執行的過程當中發現update的affected爲0 說明balance不等於100即該條數據有被其他事務更改過,此時業務上就能夠返回失敗或者從新select再計算

回滾的話,爲何只有部分 update 語句失敗,而不是整個事務裏的全部 update 都失敗?

這是由於我們的 innodb 默認是自動提交的:
須要注意的是,一般還有另一種狀況也可能致使部分語句回滾,須要格外留意。在 innodb 裏有個參數叫:innodb_rollback_on_timeout

show VARIABLES LIKE 'innodb_rollback_on_timeout'
+----------------------------+---------+
| Variable_name              | Value   |
|----------------------------+---------|
| innodb_rollback_on_timeout | OFF     |
+----------------------------+---------+
複製代碼

官方手冊裏這樣描述:
In MySQL 5.1, InnoDB rolls back only the last statement on a transaction timeout by default. If –innodb_rollback_on_timeout is specified, a transaction timeout causes InnoDB to abort and roll back the entire transaction (the same behavior as in MySQL 4.1). This variable was added in MySQL 5.1.15.

解釋:這個參數關閉或不存在的話遇到超時只回滾事務最後一個Query,打開的話事務遇到超時就回滾整個事務。

注意:

  • MySQL insert、update、replace into 死鎖回滾默認狀況下不會記錄該條 DML 語句到 binlog,也不會有回滾日誌、error ,若是不對 jdbc 返回碼作處理 Mapreduce、hive 等大數據計算任務會顯示 success 形成插入、更新部分紅功部分失敗,可是能夠從 SHOW ENGINE INNODB STATUS\G 看到數據庫的死鎖回滾日誌。這種狀況下建議根據 jdbc 錯誤碼或者 SQLException 增長重試機制或者 throw exception/error。
  • 在一個事務系統中,死鎖是確切存在而且是不能徹底避免的。 InnoDB會自動檢測事務死鎖,當即回滾其中某個事務,而且返回一個錯誤。它根據某種機制來選擇那個最簡單(代價最小)的事務來進行回滾。偶然發生的死鎖沒必要擔憂,但死鎖頻繁出現的時候就要引發注意了。InnoDB存儲引擎有一個後臺的鎖監控線程,該線程負責查看可能的死鎖問題,並自動告知用戶。

怎樣下降 innodb 死鎖概率?

死鎖在行鎖及事務場景下很難徹底消除,但能夠經過表設計和SQL調整等措施減小鎖衝突和死鎖,包括:

  • 儘可能使用較低的隔離級別,好比若是發生了間隙鎖,你能夠把會話或者事務的事務隔離級別更改成 RC(read committed)級別來避免,但此時須要把 binlog_format 設置成 row 或者 mixed 格式
  • 精心設計索引,並儘可能使用索引訪問數據,使加鎖更精確,從而減小鎖衝突的機會;
  • 選擇合理的事務大小,小事務發生鎖衝突的概率也更小;
  • 給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。好比要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖;
  • 不一樣的程序訪問一組表時,應儘可能約定以相同的順序訪問各表,對一個表而言,儘量以固定的順序存取表中的行。這樣能夠大大減小死鎖的機會;

例子:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND
至
DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;
複製代碼

儘可能用相等條件訪問數據,這樣能夠避免間隙鎖對併發插入的影響; 不要申請超過實際須要的鎖級別;除非必須,查詢時不要顯示加鎖; 對於一些特定的事務,可使用表鎖來提升處理速度或減小死鎖的可能。

詭異的 Lock wait timeout

# 默認 lock 超時時間 50s,這個時間真心不短了
show variables like 'innodb_lock_wait_timeout';
+--------------------------+---------+
| Variable_name            |   Value |
|--------------------------+---------|
| innodb_lock_wait_timeout |      50 |
+--------------------------+---------+
複製代碼

並且此次 SHOW ENGINE INNODB STATUS\G 也沒出現任何死鎖信息,而後又將目光轉向 MySQL-server 日誌,但願能從日誌裏看一看那個時刻先後數據究竟在作什麼操做。這裏先簡單的介紹下MySQL日誌文件系統的組成:

  1. error 日誌:記錄啓動、運行或中止 mysqld 時出現的問題,默認開啓。
  2. general 日誌:通用查詢日誌,記錄全部語句和指令,開啓數據庫會有 5% 左右性能損失。
  3. binlog 日誌:二進制格式,記錄全部更改數據的語句,主要用於 slave 複製和數據恢復。
  4. slow 日誌:記錄全部執行時間超過 long_query_time 秒的查詢或不使用索引的查詢,默認關閉。
  5. Innodb日誌:innodb redo log、undo log,用於恢復數據和撤銷操做。

從上面的介紹能夠看到,目前這個問題的日誌可能在 2 和 4 中,看了下 4 中沒有,那就只能開啓 2 了,但 2 對數據庫的性能有必定損耗,因爲是全量日誌,量很是巨大,因此開啓必定要謹慎:

-- general_log 日誌默認關閉,開啓會影響數據庫 5% 左右性能:
show variables like 'general%';
+------------------+---------------------------------+
| Variable_name    | Value                           |
|------------------+---------------------------------|
| general_log      | OFF                             |
| general_log_file | /opt/data/mysql/tjtx-103-26.log |
+------------------+---------------------------------+

-- 全局 session 級別開啓:
set global general_log=1

-- 若是須要對當前 session 生效須要:
set general_log=1

-- set 指令設置的動態參數在 MySQL 重啓後失效,若是須要永久生效須要在 /etc/my.cnf 中配置靜態變量/參數。
-- 若是不知道 my.cnf 位置,能夠根據 mysql -? | grep ".cnf" 查詢
                      order of preference, my.cnf, $MYSQL_TCP_PORT,
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf
複製代碼

set 指令設置的動態參數在 MySQL 重啓後失效,若是須要永久生效須要在 /etc/my.cnf 中配置靜態變量/參數。

更多內容請參考
強烈推薦-何登成的技術博客
何登成 資深技術專家 阿里巴巴數據庫內核團隊負責人,文章頗有深度

mysql死鎖問題分析
mysql中插入,更新,刪除鎖
MySQL InnoDB 鎖——官方文檔

總結

解除正在死鎖的狀態有兩種方法:

  1. 查詢是否鎖表 show OPEN TABLES where In_use > 0;
  2. 查詢進程(若是您有SUPER權限,您能夠看到全部線程。不然,您只能看到您本身的線程)
    show processlist
  3. 殺死進程id(就是上面命令的id列)
    kill id

第二種:

  1. 查看當前的事務
    SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
  2. 查看當前鎖定的事務
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
  3. 查看當前等鎖的事務
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
    殺死進程
    kill 進程ID
# 經過檢查 InnoDB_row_lock 狀態變量分析系統上中行鎖的爭奪狀況
show status like 'innodb_row_lock%';```

# 查看加鎖狀況 
show open tables where in_use > 0;

#具體使用說明可查看上文內容
show status like 'table_locks%';
show VARIABLES LIKE 'innodb_rollback_on_timeout';
show variables like 'general%';
複製代碼

相關文章
相關標籤/搜索