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

相关文章
相关标签/搜索