MySQL/InnoDB的加鎖,一直是一個常見的話題。例如,數據庫若是有高併發請求,如何保證數據完整性?產生死鎖問題如何排查並解決?下面是不一樣鎖等級的區別java
查看數據庫擁有的存儲引擎類型
SHOW ENGINES
mysql
用數據版本(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的行鎖是針對索引加的鎖,不是針對記錄加的鎖。而且該索引不能失效,不然都會從行鎖升級爲表鎖。
從上面的案例看出,行鎖變表鎖彷佛是一個坑,可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行鎖的這一特性,否則的話,可能致使大量的鎖衝突,從而影響併發性能。
行級鎖都是基於索引的,若是一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。行級鎖的缺點是:因爲須要請求大量的鎖資源,因此速度慢,內存消耗大。
死鎖(Deadlock) 所謂死鎖:是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。因爲資源佔用是互斥的,當某個進程提出申請資源後,使得有關進程在無外力協助下,永遠分配不到必需的資源而沒法繼續運行,這就產生了一種特殊現象死鎖。
解除正在死鎖的狀態有兩種方法:
第一種:
show OPEN TABLES where In_use > 0;
show processlist
kill id
第二種:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
kill 進程ID
若是系統資源充足,進程的資源請求都可以獲得知足,死鎖出現的可能性就很低,不然就會因爭奪有限的資源而陷入死鎖。其次,進程運行推動順序與速度不一樣,也可能產生死鎖。 產生死鎖的四個必要條件:
雖然不能徹底避免死鎖,但可使死鎖的數量減至最少。將死鎖減至最少能夠增長事務的吞吐量並減小系統開銷,由於只有不多的事務回滾,而回滾會取消事務執行的全部工做。因爲死鎖時回滾的操做由應用程序從新提交。
下列方法有助於最大限度地下降死鎖:
InnoDB和MyISAM的最大不一樣點有兩個:
對MyISAM表的讀操做(加讀鎖),不會阻塞其餘進程對同一表的讀操做,但會阻塞對同一表的寫操做。只有當讀鎖釋放後,才能執行其餘進程的寫操做。在鎖釋放前不能讀其餘表。
對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。
能夠利用MyISAM存儲引擎的併發插入特性,來解決應用中對同一表查詢和插入的鎖爭用。
前面講過,MyISAM 存儲引擎的讀鎖和寫鎖是互斥的,讀寫操做是串行的。那麼,一個進程請求某個 MyISAM 表的讀鎖,同時另外一個進程也請求同一表的寫鎖,MySQL 如何處理呢?
答案是寫進程先得到鎖。
不只如此,即便讀請求先到鎖等待隊列,寫請求後到,寫鎖也會插到讀鎖請求以前!這是由於 MySQL 認爲寫請求通常比讀請求要重要。這也正是 MyISAM 表不太適合於有大量更新操做和查詢操做應用的緣由,由於大量的更新操做會形成查詢操做很難得到讀鎖,從而可能永遠阻塞。這種狀況有時可能會變得很是糟糕!
幸虧咱們能夠經過一些設置來調節 MyISAM 的調度行爲。
經過指定啓動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。
SET LOWPRIORITYUPDATES=1,
使該鏈接發出的更新請求優先級下降。經過檢查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 |
+-------------------------------+-------+
複製代碼
行鎖優化
查看加鎖狀況 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 |
+----------------------------+-------+
複製代碼
此外,MyISAM的讀寫鎖調度是寫優先,這也是MyISAM不適合作寫爲主表的存儲引擎。由於寫鎖後,其餘線程不能作任何操做,大量的更新會使查詢很可貴到鎖,從而形成永久阻塞。
第一種狀況:全表更新。事務須要更新大部分或所有數據,且表又比較大。若使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突。
第二種狀況:多表查詢。事務涉及多個表,比較複雜的關聯查詢,極可能引發死鎖,形成大量事務回滾。這種狀況若能一次性鎖定事務涉及的表,從而能夠避免死鎖、減小數據庫因事務回滾帶來的開銷。
mysql 5.6 在 update 和 delete 的時候,where 條件若是不存在索引字段,那麼這個事務是否會致使表鎖? 有人回答: 只有主鍵和惟一索引纔是行鎖,普通索引是表鎖。
結果發現普通索引並不必定會引起表鎖,在普通索引中,是否引起表鎖取決於普通索引的高效程度。
上文說起的「高效」是相對主鍵和惟一索引而言,也許「高效」並非一個很好的解釋,只要明白在通常狀況下,「普通索引」效率低於其餘二者便可。 屬性值重複率高
當「值重複率」低時,甚至接近主鍵或者惟一索引的效果,「普通索引」依然是行鎖;當「值重複率」高時,MySQL 不會把這個「普通索引」當作索引,即形成了一個沒有索引的 SQL,此時引起表鎖。
同 JVM 自動優化 java 代碼同樣,MySQL 也具備自動優化 SQL 的功能。低效的索引將被忽略,這也就倒逼開發者使用正確且高效的索引。
屬性值重複率高
爲了突出效果,我將「普通索引」創建在一個「值重複率」高的屬性下。以相對極端的方式,擴大對結果的影響。
我會建立一張「分數等級表」,屬性有「id」、「score(分數)」、「level(等級)」,模擬一個半自動的業務——「分數」已被自動導入,而「等級」須要手工更新。
操做步驟以下:
取消 事務自動提交:
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,此時引起表鎖。
舉個栗子:
上面的例子,A同時收到兩筆50元轉帳,最後的餘額應該是200元,但卻由於併發的問題變爲了150元,緣由是B和C向A發起轉帳請求時,同時打開了兩個數據庫會話,進行了兩個事務,後一個事務拿到了前一個事務的中間狀態數據,致使更新丟失。
經常使用的解決思路有兩種:
要注意悲觀鎖和樂觀鎖都是業務邏輯層次的定義,不一樣的設計可能會有不一樣的實現。在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再計算
這是由於我們的 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,打開的話事務遇到超時就回滾整個事務。
注意:
死鎖在行鎖及事務場景下很難徹底消除,但能夠經過表設計和SQL調整等措施減小鎖衝突和死鎖,包括:
例子:
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 超時時間 50s,這個時間真心不短了
show variables like 'innodb_lock_wait_timeout';
+--------------------------+---------+
| Variable_name | Value |
|--------------------------+---------|
| innodb_lock_wait_timeout | 50 |
+--------------------------+---------+
複製代碼
並且此次 SHOW ENGINE INNODB STATUS\G
也沒出現任何死鎖信息,而後又將目光轉向 MySQL-server 日誌,但願能從日誌裏看一看那個時刻先後數據究竟在作什麼操做。這裏先簡單的介紹下MySQL日誌文件系統的組成:
從上面的介紹能夠看到,目前這個問題的日誌可能在 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 鎖——官方文檔
show OPEN TABLES where In_use > 0;
show processlist
kill id
第二種:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
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%';
複製代碼