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


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

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

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

3.在MIXED格式下,MySQL默認采用STATEMENT格式進行二進制日志文件的記錄。但是在一些特殊情況下會使用ROW格式,可能的情況如下:

(1)表的存儲引擎為NDB,這時對表的DML操作都會以ROW格式記錄。

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

(3) 使用了INSERT DELAY語句。

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

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

對於基於語句的日志格式(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM