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鎖機制,我這里簡單說一下,演示各種效果就讓童鞋們自己去測試了^_^
Record lock:對單個索引項加鎖
Gap lock:間隙鎖,對索引項之間的"間隙",第一條記錄前的"間隙"或最后一條記錄后的"間隙"加鎖,不包括索引項本身
Next-key lock:Gap lock+Next-key lock 鎖定索引項范圍。對記錄及其前面的間隙加鎖
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
其無法動態修改,需要修改配置文件,演示如下:
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