MySQL數據恢復和複製對InnoDB鎖機制的影響

MySQL經過BINLOG記錄執行成功的INSERT,UPDATE,DELETE等DML語句。並由此實現數據庫的恢復(point-in-time)和複製(其原理與恢復相似,經過複製和執行二進制日誌使一臺遠程的MySQLl數據庫,多稱爲slave,進行實時同步)。MySQL 5.5.x之後的版本支持3種日誌格式。經過binlog_format參數設置。該參數影響了記錄二進制日誌的格式,十分重要。php

1.STATEMENT格式和以前的MySQL版本同樣,二進制日誌文件記錄的是日誌的邏輯SQL語句。html

2.ROW格式記錄的再也不是簡單的SQL語句,而是記錄表的每行記錄更改的狀況。mysql

3.在MIXED格式下,MySQL默認採用STATEMENT格式進行二進制日誌文件的記錄。可是在一些特殊狀況下會使用ROW格式,可能的狀況以下:web

(1)表的存儲引擎爲NDB,這時對錶的DML操做都會以ROW格式記錄。算法

(2)使用了UUID(),USER(),CURRENT_USER(),FOUND_ROWS(),ROW_COUNT()等不肯定函數。sql

(3) 使用了INSERT DELAY語句。數據庫

(4)使用了用戶自定義函數(UDF).安全

(5)使用了臨時表(temporary table) 。session

對於基於語句的日誌格式(STATEMENT)的恢復和複製而言,因爲MySQL的BINLONG是按照事務(transaction)提交(committed)的前後順序記錄的,所以要正確恢復或者複製數據,就必須知足:在一個事務未提交前,其餘併發事務不能插入知足其鎖定條件的任何記錄,也就是不容許出現幻讀(Phantom Problem)。這已經超過了ISO/ANSI SQL92"可重複讀(Repeatable Read)"隔離級別的要求,其實是要求事務要串行化。這也是許多狀況下,InnoDB要用到Next-Key Lock鎖的緣由,好比用在範圍條件更新記錄時,不管是在Read Committed或者是Repeatable Read隔離級別下,InnoDB都要使用Next-key Lock鎖。既然說到Next-key Lock鎖機制,我這裏簡單說一下,演示各類效果就讓童鞋們本身去測試了^_^併發

InnoDB鎖的算法
innodb引擎有三種鎖的算法設計:
Record lock:對單個索引項加鎖
Gap lock:間隙鎖,對索引項之間的"間隙",第一條記錄前的"間隙"或最後一條記錄後的"間隙"加鎖,不包括索引項自己
Next-key lock:Gap lock+Next-key lock 鎖定索引項範圍。對記錄及其前面的間隙加鎖
 
注意:
對於惟一索引,其加上的是Record Lock,僅鎖住記錄自己。但也有特別狀況,那就是惟一索引由多個列組成,而查詢僅是查找多個惟一索引列中的其中一個,那麼加鎖的狀況依然是Next-key lock。
 
對於輔助索引,其加上的是Next-Key Lock,鎖定的是範圍,包含記錄自己。
另外若是使用相等的條件給一個不存在的記錄加鎖,innodb也會使用Next-key lock
 
特別注意:
innodb存儲引擎是經過給索引上的索引項加鎖來實現,這意味着:只有經過索引條件檢索數據,innodb纔會使用行鎖,不然,innodb將使用表鎖。(Repeatable Read隔離級別下)
若是是在表沒有主鍵或者沒有任何索引的狀況下(而且是在read committed隔離級別)。若是一個表有主鍵,沒有其餘的索引,檢索條件又不是主鍵,SQL會走聚簇索引的全掃描進行過濾,因爲過濾是由MySQL Server層面進行的。所以每條記錄,不管是否知足條件,都會被加上X鎖。可是,爲了效率考量,MySQL作了優化,對於不知足條件的記錄,會在判斷後放鎖,最終持有的,是知足條件的記錄上的鎖,可是不知足條件的記錄上的加鎖/放鎖動做不會省略。同時,優化也違背了2PL的約束。
 
對於"INSERT INTO target_tab SELECT * FROM source_tab WHERE...." 和"CREATE TABLE new_tab...SELECT....FROM source_tab WHERE...(CTAS)"這種SQL語句,用戶並無對source_tab作任何操做,可是MySQL會對這種SQL語句作特別的處理。咱們來看一個實際的例子:
mysql> select * from source_tab;
+------+------+--------+
| id   | age  | name   |
+------+------+--------+
|    1 |   24 | yayun  |
|    2 |   24 | atlas  |
|    3 |   25 | david  |
|    4 |   24 | dengyy |
+------+------+--------+
4 rows in set (0.00 sec)

mysql> select * from target_tab;
Empty set (0.00 sec)

mysql> desc source_tab;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | YES  |     | NULL    |       |
| age   | int(11)     | YES  |     | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

mysql> desc target_tab;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | YES  |     | NULL    |       |
| age   | int(11)     | YES  |     | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

mysql> 

CTAS操做給原表加鎖的例子

session1操做

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

mysql> select * from  source_tab;
+------+------+--------+
| id   | age  | name   |
+------+------+--------+
|    1 |   24 | yayun  |
|    2 |   24 | atlas  |
|    3 |   25 | david  |
|    4 |   24 | dengyy |
+------+------+--------+
4 rows in set (0.00 sec)

mysql> insert into target_tab select * from source_tab where name='yayun';         #該語句執行之後,session2中的update操做將會等待
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

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

mysql> 

session2操做

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

mysql> select * from source_tab;
+------+------+--------+
| id   | age  | name   |
+------+------+--------+
|    1 |   24 | yayun  |
|    2 |   24 | atlas  |
|    3 |   25 | david  |
|    4 |   24 | dengyy |
+------+------+--------+
4 rows in set (0.00 sec)

mysql> update source_tab set name='dengyayun' where name='yayun';  #一直等待,除非session1執行commit提交。
Query OK, 1 row affected (49.24 sec)                               #能夠看見用了49秒,這就是在等待session1提交,當session1提交後,順利更新
Rows matched: 1  Changed: 1  Warnings: 0

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

mysql> 

 在上面示例中,只是簡單的讀source_tab表的數據,至關於執行一個普通的SELECT語句,用一致性讀就能夠了。Oracle正是這麼作的,它經過MVCC技術實現的多版本併發控制實現一致性讀,不須要給source_tab加任何鎖。你們都知道InnoDB也實現了多版本併發控制(MVCC),對普通的SELECT一致性讀,也不須要加任何鎖;可是這裏InnoDB卻給source_tab表加了共享鎖,並無使用多版本一致性讀技術。

MySQL爲何這麼作呢?why?其緣由仍是爲了保證恢復和複製的正確性。由於在不加鎖的狀況下,若是上述語句執行過程當中,其餘事務對原表(source_tab)作了更新操做,就可能致使數據恢復結果錯誤。爲了演示錯誤的發生,再重複上面的例子,先將系統變量innodb_locks_unsafe_for_binlog的值設爲"on",默認值是off

innodb_locks_unsafe_for_binlog

設定InnoDB是否在搜索和索引掃描中使用間隙鎖(gap locking)。InnoDB使用行級鎖(row-level locking),一般狀況下,InnoDB在搜索或掃描索引的行鎖機制中使用「下一鍵鎖定(next-key locking)」算法來鎖定某索引記錄及其前部的間隙(gap),以阻塞其它用戶緊跟在該索引記錄以前插入其它索引記錄。站在這個角度來講,行級鎖也叫索引記錄鎖(index-record lock)。
默認狀況下,此變量的值爲OFF,意爲禁止使用非安全鎖,也即啓用間隙鎖功能。將其設定爲ON表示禁止鎖定索引記錄前的間隙,也即禁用間隙鎖,InnoDB僅使用索引記錄鎖(index-record lock)進行索引搜索或掃描,不過,這並不由止InnoDB在執行外鍵約束檢查或重複鍵檢查時使用間隙鎖。
啓用innodb_locks_unsafe_for_binlog的效果相似於將MySQL的事務隔離級別設定爲READ-COMMITTED,但兩者並不徹底等同:innodb_locks_unsafe_for_binlog是全局級別的設定且只能在服務啓動時設定,而事務隔離級別可全局設定並由會話級別繼承,然而會話級別也以按需在運行時對其進行調整。相似READ-COMMITTED事務隔離級別,啓用innodb_locks_unsafe_for_binlog也會帶來「幻影問題(phantom problem)」,但除此以外,它還能帶來以下特性:
(1)對UPDATE或DELETE語句來講,InnoDB僅鎖定須要更新或刪除的行,對不可以被WHERE條件匹配的行施加的鎖會在條件檢查後予以釋放。這能夠有效地下降死鎖出現的機率;
(2)執行UPDATE語句時,若是某行已經被其它語句鎖定,InnoDB會啓動一個「半一致性(semi-consistent)」讀操做從MySQL最近一次提交版本中得到此行,並以之斷定其是否可以並當前UPDATE的WHERE條件所匹配。若是可以匹配,MySQL會再次對其進行鎖定,而若是仍有其它鎖存在,則須要先等待它們退出。

其沒法動態修改,須要修改配置文件,演示以下:

CTAS操做不給原表加鎖帶來的安全問題

mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | MIXED |
+---------------+-------+
1 row in set (0.00 sec)

mysql> show variables like 'innodb_locks_unsafe%';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_locks_unsafe_for_binlog | ON    |
+--------------------------------+-------+
1 row in set (0.00 sec)

mysql> 

session1操做

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

mysql> select * from source_tab where id=1;
+------+------+-----------+
| id   | age  | name      |
+------+------+-----------+
|    1 |   24 | dengyayun |
+------+------+-----------+
1 row in set (0.00 sec)

mysql> insert into target_tab select * from source_tab where id=1;        
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> commit;    #插入操做後提交
Query OK, 0 rows affected (0.01 sec)

mysql> select * from source_tab where name='good yayun'; #此時查看數據,target_tab中能夠插入source_tab更新前的結果,這複合應用邏輯
+------+------+------------+
| id   | age  | name       |
+------+------+------------+
|    1 |   24 | good yayun |
+------+------+------------+
1 row in set (0.00 sec)

mysql> select * from target_tab;
+------+------+-----------+
| id   | age  | name      |
+------+------+-----------+
|    1 |   24 | dengyayun |
+------+------+-----------+
1 row in set (0.00 sec)

session2操做

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

mysql> select * from source_tab where id=1;
+------+------+-----------+
| id   | age  | name      |
+------+------+-----------+
|    1 |   24 | dengyayun |
+------+------+-----------+
1 row in set (0.00 sec)

mysql> update source_tab set name='good yayun' where id=1;  # session1未提交,能夠對session1中的select記錄進行更新操做
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;       # 更新操做先提交
Query OK, 0 rows affected (0.02 sec)

mysql> select * from source_tab where name='good yayun';
+------+------+------------+
| id   | age  | name       |
+------+------+------------+
|    1 |   24 | good yayun |
+------+------+------------+
1 row in set (0.00 sec)

mysql> select * from target_tab;
+------+------+-----------+
| id   | age  | name      |
+------+------+-----------+
|    1 |   24 | dengyayun |
+------+------+-----------+
1 row in set (0.00 sec)

mysql> 

從上面的測試結果能夠發現,設置系統變量innodb_locks_unsafe_for_binlog的值爲"ON"後,innodb再也不對原表(source_tab)加鎖,結果也符合應用的邏輯,可是若是咱們分析一下BINLOG內容,就能夠發現問題所在

[root@MySQL-01 mysql]# mysqlbinlog mysql-bin.000120 | grep -A 20 'update source_tab' update source_tab set name='good yayun' where id=1
/*!*/;
# at 468
#140401  2:04:12 server id 1  end_log_pos 495   Xid = 74
COMMIT/*!*/;
# at 495
#140401  2:04:23 server id 1  end_log_pos 563   Query   thread_id=5     exec_time=0     error_code=0
SET TIMESTAMP=1396289063/*!*/;
BEGIN
/*!*/;
# at 563
#140401  2:02:42 server id 1  end_log_pos 684   Query   thread_id=5     exec_time=0     error_code=0
SET TIMESTAMP=1396288962/*!*/;
insert into target_tab select * from source_tab where id=1
/*!*/;
# at 684
#140401  2:04:23 server id 1  end_log_pos 711   Xid = 73
COMMIT/*!*/;
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
[root@MySQL-01 mysql]# 

能夠清楚的看到在BINLOG的記錄中,更新操做的位置在INSERT......SELECT以前,若是使用這個BINLOG進行數據庫恢復,恢復的結果則與實際的應用邏輯不符;若是進行復制,就會致使主從數據不一致!

經過上面的例子,相信童鞋們不難理解爲何MySQL在處理

"INSERT INTO target_tab SELECT * FROM source_tab WHERE...."

"CREATE TABLE new_tab....SELECT.....FROM source_tab WHERE...."

時要給原表(source_tab)加鎖,而不是使用對併發影響最小的多版本數據來實現一致性讀。還要特別說明的是,若是上述語句的SELECT是範圍條件,innodb還會給原表加上Next-Key Lock鎖。

所以,INSERT....SELECT和CREATE TABLE....SELECT.....語句,可能會阻止對原表的併發更新。若是查詢比較複雜,會照成嚴重的性能問題,生產環境須要謹慎使用。

總結以下:

若是應用中必定要用這種SQL來實現業務邏輯,又不但願對源表的併發更新產生影響,可使用下面3種方法:

1.將innodb_locks_unsafe_for_binlog的值設置爲"ON",強制MySQL使用多版本數據一致性讀。但付出的代價是可能沒法使用BINLOG正確的進行數據恢復或者主從複製。所以,此方法是不推薦使用的。

2.經過使用SELECT * FROM source_tab ..... INTO OUTFILE 和LOAD DATA INFILE.....語句組合來間接實現。採用這種放鬆MySQL不會給(源表)source_tab加鎖。

3.使用基於行(ROW)的BINLOG格式和基於行的數據的複製。此方法是推薦使用的方法。

 

參考資料:

https://www.facebook.com/note.php?note_id=131719925932

http://dev.mysql.com/doc/refman/5.0/en/innodb-parameters.html#sysvar_innodb_locks_unsafe_for_binlog

相關文章
相關標籤/搜索