MySQL 5.7 InnoDB鎖

簡介
參考https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-gap-locks。
InnoDB引擎實現了標準的行級別鎖(S和X)。InnoDB引擎加鎖原則遵循二段鎖協議,即事務分爲兩個階段,事務開始後進入加鎖階段,事務commit或者rollback就進入解鎖階段。InnoDB引擎下鎖的影響因素不少,隔離級別不一樣,是否使用索引等都會產生不一樣的鎖結果。
 
查看鎖和事務
當出現ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,要解決是一件麻煩的事情。特別是當一個SQL執行完了,但未COMMIT,後面的SQL想要執行就是被鎖,超時後結束,DBA光從數據庫沒法着手找出源頭是哪一個SQL鎖住了。有時候看看 show engine innodb status, 並結合 show full processlist 能暫時解決問題,但一直不能精肯定位。在5.5中,information_schema 庫中增長了三個關於鎖的表(MEMORY引擎)。
 
INNODB_LOCKS
提供有關InnoDB事務已請求但還沒有得到的以及事務正在阻塞另外一個事務的鎖的信息。
lock_id
InnoDB內部的惟一鎖ID
lock_trx_id
擁有這個鎖的事務ID
lock_mode
請求的鎖,S, X, IS, IX等
lock_type
鎖類型,RECORD或者TABLE 
lock_table
被鎖的表或包含被鎖記錄的表
lock_index
被鎖的索引,不是行級鎖時爲NULL
lock_space
被鎖的表空間號,不是行級鎖時爲NULL
lock_page
被鎖的頁號,不是行級鎖時爲NULL
lock_rec
被鎖的Heap號,不是行級鎖時爲NULL
lock_data
被鎖的記錄的主鍵,不是行級鎖時爲NULL
 
INNODB_LOCK_WAITS
當前等待的鎖。
requesting_trx_id
正在請求的、受阻的事務ID
requested_lock_id
事務正在等待的鎖ID
blocking_trx_id
阻塞其餘事務的事務ID
blocking_lock_id 
阻塞其餘事務的事務持有的鎖ID
 
INNODB_TRX
當前事務。
trx_id 
InnoDB內部的惟一事務ID
trx_state
事務狀態,RUNNING, LOCK WAIT等
trx_started
事務開始時間
trx_requested_lock_id
事務正在等待的鎖ID
trx_wait_started
事務開始等待的時間
trx_weight
事務的權重,當發生死鎖回滾的時候,優先選擇該值最小的進行回滾
trx_mysql_thread_id
事務線程ID,即show full processlist中的ID
trx_query
執行的SQL語句
trx_operation_state
事務當前操做狀態
trx_tables_in_use
執行當前SQL時有多少個表被使用
trx_tables_locked
執行當前SQL時有多少個表有行鎖
trx_lock_structs
事務保留的鎖的數量
trx_lock_memory_bytes
事務鎖佔據的內存大小(B)
trx_rows_locked
事務鎖定的大概行數
trx_rows_modified
事務修改和插入的行數
trx_concurrency_tickets
即innodb_concurrency_tickets系統變量
trx_isolation_level
事務隔離級別
trx_unique_checks
是否惟一性檢查
trx_foreign_key_checks
是否外鍵檢查
trx_last_foreign_key_error
最後的外鍵錯誤詳細信息
trx_adaptive_hash_latched
 
trx_adaptive_hash_timeout
 
trx_is_read_only
1表示事務是隻讀的
trx_autocommit_non_locking
1表示事務是不包含FOR UPDATE或者LOCK IN SHARE MODE語句,而且autocommit是enable的
 
強行解鎖
一、
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
獲取到blocking_trx_id
 
二、
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
查找trx_id和上面獲取到的blocking_trx_id同樣的記錄,獲取這條記錄的trx_mysql_thread_id
 
三、
kill 上面獲取到的trx_mysql_thread_id,這樣就把阻塞其餘事務的事務線程殺掉了。
 
共享鎖和排它鎖
共享鎖(S lock):容許持有鎖的事務讀取一行,select語句後加lock in share mode。
排它鎖(X lock):容許持有鎖的事務更新或刪除一行,select語句後加for update。
共享鎖和排他鎖都是鎖的行記錄。當一個事務獲取了行r的共享鎖,那麼另一個事務也能夠當即獲取行r的共享鎖,由於讀取並未改變行r的數據,這種狀況就是鎖兼容。
可是若是有事務想得到行r的排它鎖,則它必須等待其餘事務釋放行r上的共享鎖,這種狀況就是鎖不兼容。兩者兼容性以下表格所示:
 
X
S
X
衝突
衝突
S
衝突
兼容
對於select 語句,InnoDB不會加任何鎖,也就是能夠多個併發去進行select的操做,不會有任何的鎖衝突,由於根本沒有鎖。
對於insert,update,delete操做,InnoDB會自動給涉及到的數據加排他鎖,只有查詢select須要咱們手動設置排他鎖。
 
意向鎖
InnoDB支持多粒度鎖定,容許行鎖和表鎖共存。例如 LOCK TABLES ... WRITE 這樣的語句會獲取指定表的排它鎖。爲了在多個粒度級別上實現鎖定,InnoDB使用了意向鎖。意向鎖是表級鎖,指示事務稍後事務對錶的行須要的鎖的類型(共享鎖或排它鎖)。意向鎖有兩種:
意向共享鎖(IS):指示事務打算在表中的單個行上設置共享鎖。若是須要對記錄A加共享鎖,那麼此時InnoDB會先找到這張表,對該表加意向共享鎖以後,再對記錄A添加共享鎖。
意向排它鎖(IX):指示事務打算在表中的單個行上設置排他鎖。若是須要對記錄A加排他鎖,那麼此時InnoDB會先找到這張表,對該表加意向排他鎖以後,再對記錄A添加排他鎖。
例如 SELECT ... LOCK IN SHARE MODE 設置IS,SELECT ... FOR UPDATE 設置IX。這兩種意向鎖都是表鎖,都是系統自動添加和自動釋放的,整個過程無需人工干預。
意圖鎖定協議以下:
在事務能夠獲取表中某一行上的共享鎖以前,它必須首先獲取表上的IS鎖或比IS鎖更強的鎖;
在事務能夠獲取表中某一行上的排他鎖以前,它必須首先獲取表上的IX鎖。
表級鎖類型兼容性總結以下:
 
X
IX
S
IS
X
衝突
衝突
衝突
衝突
IX
衝突
兼容
衝突
兼容
S
衝突
衝突
兼容
兼容
IS
衝突
兼容
兼容
兼容
若是請求事務與現有鎖兼容,則授予該事務鎖,但若是與現有鎖衝突,則不授予該事務鎖。事務等待衝突的現有鎖被釋放。若是鎖請求與現有鎖衝突,而且因爲會致使死鎖而沒法被授予,則會發生錯誤。意向鎖不會阻塞除全表鎖請求(例如 LOCK TABLES ... WRITE)以外的任何東西,意向鎖定的主要目的是顯示某人鎖定了一行,或者準備鎖定表中的一行。
由於表鎖覆蓋了行鎖的數據,因此表鎖和行鎖會產生衝突。好比A事務申請表鎖,B事務申請行級鎖,或者A事務申請行級鎖,B事務申請表鎖。這時候B事務的申請是須要被阻塞的。那麼怎麼判斷B事務該阻塞呢?遍歷表的每一行看看是否有行級鎖嗎?這樣效率很是差。這時候就引入了意向鎖。在申請行鎖前,數據庫自動爲咱們申請了對應的意向鎖,由於意向鎖是表鎖,這時候若是再申請表鎖,就天然會阻塞了。意向鎖之間互相兼容,表鎖(ALTER TABLE, DROP TABLE, LOCK TABLES等)會和意向鎖衝突。
執行 SHOW ENGINE INNODB STATUS\G,若是有意向鎖,就能夠看到相似下面的信息:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
 
記錄鎖
記錄鎖是索引記錄上的鎖,記錄鎖老是鎖定索引記錄,即便表沒有定義索引。對於這種狀況,InnoDB建立一個隱藏的聚簇索引,並使用這個索引來鎖定記錄。
對主鍵加鎖,加在聚簇索引上;
對二級索引加鎖,加在二級索引+聚簇索引上;
對無索引列加鎖,加在聚簇索引上。
執行 SHOW ENGINE INNODB STATUS\G,若是有記錄鎖,就能夠看到相似下面的信息:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
 
取消自動提交
InnoDB下事務一旦提交或者回滾,就會自動釋放事務中的鎖。變量autocommit=1開啓自動提交,autocommit=0關閉自動提交,默認autocommit=1。在自動提交模式下,每執行一句sql,就自動提交事務,鎖也會當即釋放。這種狀況下沒法手動控制事務的提交以及鎖的釋放時間。
所以,咱們會在進行鎖的相關操做以前,先執行(set autocommit=0)或者start transaction以關閉自動提交模式,開啓手動提交模式,這樣事務中的每一行sql執行完成後,鎖一直不會釋放,直到咱們手動提交或者回滾事務,鎖纔會釋放。
 
間隙鎖(GAP)
間隙鎖是對索引記錄之間的間隙的鎖,或對第一個索引記錄以前或最後一個索引記錄以後的間隙的鎖,不包含索引記錄自己。間隙鎖是性能和併發性之間權衡的一部分,用於某些事務隔離級別,而不是其餘事務隔離級別。
對於使用唯一索引鎖定行以搜索唯一行的語句,不須要間隙鎖(這並不包括搜索條件只包含一個複合多列唯一索引的某些列的狀況,在這種狀況下,確實會發生間隙鎖)。若是列沒有索引,或者索引不是唯一的,那麼語句將產生間隙鎖。
必須在REPEATABLE-READ級別纔可使用間隙鎖。能夠顯式禁用間隙鎖,若是將事務隔離級別更改成READ COMMITTED或啓用innodb_locks_unsafe_for_binlog系統變量(如今已經不推薦使用該變量),就會發生這種狀況。在這種狀況下,對搜索和索引掃描禁用間隙鎖,只用於外鍵約束檢查和重複鍵檢查。
如何肯定間隙鎖的區間?根據檢索索引記錄C向左尋找最靠近C的索引記錄值A,做爲左區間,向右尋找最靠近C的索引記錄值B做爲右區間,即鎖定的間隙爲(A,B),A<C<B,鎖定的間隙除了索引記錄C之間還包括第一個索引記錄A以前和最後一個索引記錄B以後。索引記錄相同的值是根據主鍵升序排序的。
間隙鎖的目的是爲了防止幻讀,其主要經過兩個方面實現這個目的:
一、防止兩個索引區間內有新數據被插入。
二、防止現有數據更新成兩個索引區間內的數據。
 
Next-Key鎖
Next-Key鎖是索引記錄上的記錄鎖(S或X)和索引記錄前的間隙上的間隙上的間隙鎖的組合。通常來講MySQL用的都是Next-Key鎖。Next-Key鎖是左開右閉的區間,例如索引包含十、十一、1三、20,那麼可能的Next-Key鎖以下:
( 負無窮大, 10]
(10, 11]
(11, 13]
(13, 20]
(20, 正無窮大)
 
測試環境和數據
MySQL版本爲5.7.21,部署於CentOS X86_64上,採用默認配置。
 
建立表
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
 
 
數據
INSERT INTO `test` (`id`,`age`) VALUES (6,1);
INSERT INTO `test` (`id`,`age`) VALUES (8,5);
INSERT INTO `test` (`id`,`age`) VALUES (10,8);
INSERT INTO `test` (`id`,`age`) VALUES (12,12);
INSERT INTO `test` (`id`,`age`) VALUES (14,14);
INSERT INTO `test` (`id`,`age`) VALUES (16,14);
INSERT INTO `test` (`id`,`age`) VALUES (18,18);
INSERT INTO `test` (`id`,`age`) VALUES (20,20);
 
+----+-----+
| id | age |
+----+-----+
| 6 | 1 |
| 8 | 5 |
| 10 | 8 |
| 12 | 12 |
| 14 | 14 |
| 16 | 14 |
| 18 | 18 |
| 20 | 20 |
+----+-----+
 
示例
啓動三個會話,會話1和會話2均取消自動提交,會話3查看鎖狀態。
SELECT * FROM test where age=14 for update 語句的間隙鎖範圍(id,age)包括(12,12)到(18,18)之間的間隙,同時還會對記錄age=14加排他鎖。
 
1、
會話1:
SELECT * FROM test where age=14 for update;
會話2:
INSERT INTO `test` (`id`, `age`) VALUES (13, 12);
結果:
會話2阻塞。
 
2、
會話1:
SELECT * FROM test where age=14 for update;
會話2:
INSERT INTO `test` (`id`, `age`) VALUES (17, 18);
結果:
會話2阻塞。
 
3、
會話1:
SELECT * FROM test where age=14 for update;
會話2:
INSERT INTO `test` (`id`, `age`) VALUES (11, 12);
結果:
會話2執行成功。
 
4、
會話1:
SELECT * FROM test where age=14 for update;
會話2:
INSERT INTO `test` (`id`, `age`) VALUES (11, 14);
結果:
會話2阻塞。
 
5、
會話1:
SELECT * FROM test where age=14 for update;
會話2:
UPDATE `test` SET `age`=14 WHERE `id`=18;;
結果:
會話2阻塞。
 
5、
會話1:
SELECT * FROM test where age=14 for update;
會話2:
UPDATE `test` SET `age`=19 WHERE `id`=18;;
結果:
會話2執行成功。
相關文章
相關標籤/搜索