1. 程式人生 > >INNODB鎖(2)

INNODB鎖(2)

在上一篇文章寫了鎖的基本概述以及行鎖的三種形式,這一篇的主要內容如下:

  • 一致性非鎖定讀
  • 自增長與鎖
  • 外來鍵和鎖

一致性性非鎖定讀

一致性非鎖定讀是InnoDB通過多版本併發控制(MVCC,multi version concurrency control)的方式來讀取當前執行時間資料庫中的最近一次快照,如果讀取的行正在執行DELETE、UPDATE操作,這時讀取操作不會等待行上鎖的釋放,相反,InnoDB儲存引擎會去讀取行的一個快照資料,如下圖:

上圖直觀地展示了InnoDB儲存引擎一致性的非鎖定讀,之所以稱其為非鎖定讀,因為不需要等待訪問的行上X鎖的釋放。快照資料是指該行之前版本的資料,該實現是通過Undo段來實現,而Undo用來在事務中回滾資料,因此快照資料本身是沒有額外的開銷。此外,讀取快照資料是不需要上鎖的,因為沒有必要對歷史的資料進行修改。

可以看到,非鎖定讀的機制大大提高了資料讀取的併發性,在InnoDB儲存因為預設設定下,這是預設的讀取方式,即讀取不會佔用和等待表上的鎖。但是在不同事務隔離級別下,讀取的方式不同,並不是每個事務隔離級別下讀取的都是一致性讀。同樣,即使都是使用一致性讀,但是對於快照資料的定義也不相同。

通過上圖,我們可以看出快照資料其實就是當前資料之前的歷史版本,可能有多個版本。一個行可能又不止一個快照資料。我們稱這種技術為行多版本技術。由此帶來的併發控制,稱之為多版本併發控制(MVCC,multi version concurrency control

在READ COMMITTED和REPEATABLE READ下,InnoDB儲存引擎使用非鎖定的一致性讀。然而,對於快照資料的定義卻不相同。在READ COMMITTED

事務隔離級別下,對於快照資料,非一致性讀總是讀取被鎖定行的最新一份快照資料。在REPEATABLE READ事務隔離級別下,對於快照資料,非一致性讀總是讀取事務開始時的行資料版本。下面看一個列子

時間序列 會話A 會話B
1 mysql> begin;   #開啟一個事務
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tb1 where a = 5;
+---+
| a |
+---+
| 5 |
+---+
1 row in set (0.00 sec)


 
2  

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update tb1 set a = 13 where a = 5;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

#開啟一個事務B,更新同一條資料

3

#這時候RR和RC隔離級別,查詢到的資料都是如下(都解決了髒讀問題):

mysql> select * from tb1 where a = 5;
+---+
| a |
+---+
| 5 |
+---+
1 row in set (0.00 sec)

 
4  

#提交事務

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

5

#在RR的隔離級別下資料讀到的資料如下:讀取事務開始時的版本

mysql> select * from tb1 where a = 5;
+---+
| a |
+---+
| 5 |
+---+
1 row in set (0.00 sec)

 
6

#在RC的隔離級別下讀到的資料如下:總是讀取最新的一份快照資料。

mysql> select * from tb1 where a = 5;
Empty set (0.00 sec)

#這裡我們提到過,同一個事務中兩次讀到的資料並不一樣,其實違反了事務的隔離性,出現了幻讀!

 

 自增長和鎖

自增長在資料庫中是非常常見的一種屬性,也是很多DBA或開發人員首選的主鍵方式。在InnoDB儲存引擎的記憶體結構中,對每個含有自增長值的表都有一個自增長計數器。當對含有自增長的計數器的表進行插入操作時,這個計數器會被初始化,執行如下的語句來得到計數器的值:

select max(auto_inc_col) from t for update;

插入操作會依據這個自增長的計數器值加1賦予自增長列。這個實現方式稱為AUTO-INC Locking。這種鎖其實是採用一種特殊的表鎖機制,為了提高插入的效能,鎖不是在一個事務完成後才釋放,而是在完成對自增長值插入的SQL語句後立即釋放。【注意自增鎖釋放的時機

雖然AUTO-INC Locking從一定程度上提高了併發插入的效率,但還是存在一些效能上的問題。首先,對於有自增長值的列的併發插入效能較差,事務必須等待前一個插入的完成,雖然不用等待事務的完成。其次,對於INSERT….SELECT的大資料的插入會影響插入的效能,因為另一個事務中的插入會被阻塞。

從MySQL 5.1.22版本開始,InnoDB儲存引擎中提供了一種輕量級互斥量的自增長實現機制,這種機制大大提高了自增長值插入的效能。並且從該版本開始,InnoDB儲存引擎提供了一個引數innodb_autoinc_lock_mode來控制自增長的模式,該引數的預設值為1。在繼續討論新的自增長實現方式之前,需要對自增長的插入進行分類。如下說明:

  • insert-like:指所有的插入語句,如INSERT、REPLACE、INSERT…SELECT,REPLACE…SELECT、LOAD DATA等。
  • simple inserts:指能在插入前就確定插入行數的語句,這些語句包括INSERT、REPLACE等。需要注意的是:simple inserts不包含INSERT…ON DUPLICATE KEY UPDATE這類SQL語句。
  • bulk inserts:指在插入前不能確定得到插入行數的語句,如INSERT…SELECT,REPLACE…SELECT,LOAD DATA。
  • mixed-mode inserts:指插入中有一部分的值是自增長的,有一部分是確定的。入INSERT INTO t1(c1,c2) VALUES(1,’a’),(2,’a’),(3,’a’);也可以是指INSERT…ON DUPLICATE KEY UPDATE這類SQL語句。

接下來分析引數innodb_autoinc_lock_mode以及各個設定下對自增長的影響,其總共有三個有效值可供設定,即0、1、2,具體說明如下:

  • 0:這是MySQL 5.1.22版本之前自增長的實現方式,即通過表鎖的AUTO-INC Locking方式,因為有了新的自增長實現方式,0這個選項不應該是新版使用者的首選了。
  • 1:這是該引數的預設值,對於”simple inserts”,該值會用互斥量(mutex)去對記憶體中的計數器進行累加的操作。對於”bulk inserts”,還是使用傳統表鎖的AUTO-INC Locking方式。在這種配置下,如果不考慮回滾操作,對於自增值列的增長還是連續的。並且在這種方式下,statement-based方式的replication還是能很好地工作。需要注意的是,如果已經使用AUTO-INC Locing方式去產生自增長的值,而這時需要再進行”simple inserts”的操作時,還是需要等待AUTO-INC Locking的釋放。
  • 2:在這個模式下,對於所有”INSERT-LIKE”自增長值的產生都是通過互斥量,而不是AUTO-INC Locking的方式。顯然,這是效能最高的方式。然而,這會帶來一定的問題,因為併發插入的存在,在每次插入時,自增長的值可能不是連續的。此外,最重要的是,基於Statement-Base Replication會出現問題。因此,使用這個模式,任何時候都應該使用row-base replication。這樣才能保證最大的併發效能及replication主從資料的一致。
mysql> show variables like "innodb_autoinc_lock_mode";         #這個數值預設是1,並且是個只讀的變數,不能改變,可以從原始碼改變
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 1     |
+--------------------------+-------+
1 row in set (0.00 sec)

mysql> set global innodb_autoinc_lock_mode = 2;
ERROR 1238 (HY000): Variable 'innodb_autoinc_lock_mode' is a read only variable

 

這裡需要特別注意,InnoDB跟MyISAM不同,MyISAM儲存引擎是表鎖設計,自增長不用考慮併發插入的問題。因此在master上用InnoDB儲存引擎,在slave上用MyISAM儲存引擎的replication架構下,使用者可以考慮這種情況。

另外,InnoDB儲存引擎,自增持列必須是索引,同時必須是索引的第一個列,如果不是第一個列,會丟擲異常,而MyiSAM不會有這個問題。

外來鍵和鎖:

簡單說一下外來鍵,外來鍵主要用於引用完整性的約束檢查。在InnoDB儲存引擎中,對於一個外來鍵列,如果沒有顯示地對這個列加索引,InnoDB儲存引擎會自動對其加一個索引,因為這樣可以避免表鎖。這比Oracle資料庫做得好,Oracle資料庫不會自動新增索引,使用者必須自己手動新增,這也導致了Oracle資料庫中可能產生死鎖。

對於外來鍵值的插入或更新,首先需要檢查父表中的記錄,既SELECT父表。但是對於父表的SELECT操作,不是使用一致性非鎖定讀的方式,因為這會發生資料不一致的問題,因此這時使用的是SELECT…LOCK IN SHARE MODE方式,即主動對父表加一個S鎖。如果這時父表上已經這樣加X鎖,子表上的操作會被阻塞,如下:

例項如下:

# 建立parent表;
create table parent(
  tag_id int primary key auto_increment not null,
  tag_name varchar(20)
);
 
# 建立child表;
create table child(
  article_id int primary key auto_increment not null,
  article_tag int(11),
  CONSTRAINT  tag_at FOREIGN KEY (article_tag) REFERENCES parent(tag_id)
);
 
# 插入資料;
insert into parent(tag_name) values('mysql');
insert into parent(tag_name) values('oracle');
insert into parent(tag_name) values('mariadb');

開始測試

# Session A
mysql> begin
mysql> delete from parent where tag_id = 3;
 
# Session B
mysql> begin
mysql> insert into child(article_id,article_tag) values(1,3);   #阻塞

第二列是外來鍵,執行該語句時被阻塞。

在上述的例子中,兩個會話中的事務都沒有進行COMMIT或ROLLBACK操作,而會話B的操作會被阻塞。這是因為tag_id為3的父表在會話中已經加了一個X鎖,而此時在會話B中使用者又需要對父表中tag_id為3的行加一個S鎖,這時INSERT的操作會被阻塞。設想如果訪問父表時,使用的是一致性的非鎖定讀,這時Session B會讀到父表有tag_id=3的記錄,可以進行插入操作。但是如果會話A對事務提交了,則父表中就不存在tag_id為3的記錄。資料在父、子表就會存在不一致的情況。若這時使用者查詢INNODB_LOCKS表,會看到如下結果:

mysql> select * from information_schema.innodb_locks\G
*************************** 1. row ***************************
    lock_id: 3359:35:3:4
lock_trx_id: 3359
  lock_mode: S
  lock_type: RECORD
 lock_table: `test`.`parent`
 lock_index: PRIMARY
 lock_space: 35
  lock_page: 3
   lock_rec: 4
  lock_data: 3
*************************** 2. row ***************************
    lock_id: 3358:35:3:4
lock_trx_id: 3358
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`parent`
 lock_index: PRIMARY
 lock_space: 35
  lock_page: 3
   lock_rec: 4
  lock_data: 3
2 rows in set, 1 warning (0.00 sec)

從鎖結構可以看出,對於parent表加了兩個鎖,一個S鎖和一個X鎖。

博文基本摘自inside君的《MySQL技術內幕--INNODB儲存引擎》,實際地址來自:http://www.ywnds.com/?p=9129