mysql批量更新方法
实验mysql版本为5.7.20 隔离级别为rr,加锁场景的问题在mysql8.0.18中为复现
方法一 replace into 批量更新
原理:replace into table (col1,col2) values (x1,x2), 操作本质是对重复的记录先delete 后insert
缺点:1、如果更新的字段不全会将缺失的字段置为缺省值
2、如果表中存在多个唯一键约束,那么会删除多行,造成数据丢失
3、当有并发场景时,容易造成锁表,replace into流程死锁分析:http://mysql.taobao.org/monthly/2015/03/01/
字段缺失实验:
- CREATE TABLE `t_insert` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `i1` int(11) NOT NULL DEFAULT '0',
- `i2` int(11) NOT NULL DEFAULT '0',
- PRIMARY KEY (`id`),
- UNIQUE KEY `idx_i1` (`i1`),
- UNIQUE KEY `idx_i2` (`i2`) USING BTREE
- ) ENGINE=InnoDB AUTO_INCREMENT=1;
- mysql> select * from t_insert limit 4;
- +-----+-----+-----+
- | id | i1 | i2 |
- +-----+-----+-----+
- | 107 | 102 | 211 |
- | 109 | 103 | 203 |
- | 111 | 10 | 99 |
- | 113 | 105 | 205 |
- +-----+-----+-----+
- 4 rows in set (0.00 sec)
- 没有设置i2的值
- mysql> replace into t_insert(id,i1) values (107,109);
- Query OK, 2 rows affected (0.04 sec)
- mysql> select * from t_insert limit 4;
- +-----+-----+-----+
- | id | i1 | i2 |
- +-----+-----+-----+
- | 107 | 109 | 0 | i2默认值为0
- | 109 | 103 | 203 |
- | 111 | 10 | 99 |
- | 113 | 105 | 205 |
- +-----+-----+-----+
- 4 rows in set (0.00 sec)
丢失数据实验:
- mysql> describe t_insert;
- +-------+------------------+------+-----+---------+----------------+
- | Field | Type | Null | Key | Default | Extra |
- +-------+------------------+------+-----+---------+----------------+
- | id | int(10) unsigned | NO | PRI | NULL | auto_increment |
- | i1 | int(11) | NO | UNI | 0 | |
- | i2 | int(11) | NO | UNI | 0 | |
- +-------+------------------+------+-----+---------+----------------+
- 3 rows in set (0.12 sec)
- mysql> select * from t_insert;
- +-----+-----+-----+
- | id | i1 | i2 |
- +-----+-----+-----+
- | 107 | 109 | 0 |
- | 109 | 103 | 203 |
- | 111 | 10 | 99 |
- | 113 | 105 | 205 |
- +-----+-----+-----+
- 4 rows in set (0.00 sec)
- mysql> replace into t_insert(id,i1,i2) values (107,103,205);
- Query OK, 4 rows affected (0.03 sec)
- 插入的数据存在3个冲突键,删掉3行,插入一行。造成数据丢失
- mysql> select * from t_insert;
- +-----+-----+-----+
- | id | i1 | i2 |
- +-----+-----+-----+
- | 107 | 103 | 205 |
- | 111 | 10 | 99 |
- +-----+-----+-----+
- 2 rows in set (0.00 sec)
不指定id,无论是否有唯一键重复自增值就会自增实验:
- mysql> select * from t_insert;
- +----+----+----+
- | id | i1 | i2 |
- +----+----+----+
- | 4 | 1 | 1 |
- +----+----+----+
- 1 row in set (0.01 sec)
- mysql> SELECT Auto_increment FROM information_schema.TABLES WHERE Table_Schema='hi' AND table_name='t_insert';
- +----------------+
- | Auto_increment |
- +----------------+
- | 5 |
- +----------------+
- 1 row in set (0.00 sec)
- 插入重复键1,1
- mysql> replace into t_insert(i1,i2) values(1,1) ;
- Query OK, 2 rows affected (0.01 sec)
- mysql> select * from t_insert;
- +----+----+----+
- | id | i1 | i2 |
- +----+----+----+
- | 5 | 1 | 1 |
- +----+----+----+
- 1 rows in set (0.01 sec)
- mysql> SELECT Auto_increment FROM information_schema.TABLES WHERE Table_Schema='hi' AND table_name='t_insert';
- +----------------+
- | Auto_increment |
- +----------------+
- | 6 |
- +----------------+
- 1 row in set (0.00 sec)
- 插入新值
- mysql> replace into t_insert(i1,i2) values(2,2) ;
- Query OK, 1 row affected (0.01 sec)
- mysql> SELECT Auto_increment FROM information_schema.TABLES WHERE Table_Schema='hi' AND table_name='t_insert';
- +----------------+
- | Auto_increment |
- +----------------+
- | 7 |
- +----------------+
- 1 row in set (0.00 sec)
- mysql> replace into t_insert(i1,i2) values (110,220);
- Query OK, 2 rows affected (0.13 sec)
分析死锁前先介绍一些Mysql锁相关知识
在rr隔离级别中,普通的insert 插入前需要LOCK_INSERT_INTENTION 插入意向锁,插入意向锁是为了提供并发插入的性能,也是一种间隙锁,多个事务同时写入不同数据至同一索引范围(区间)内,并不需要等待其他事务完成,不会发生锁等待,同时插入意向锁是会被gap锁锁住的,在持有gap锁的情况下,新insert插入到这个范围时会获取这个范围的插入意向锁,此时如果这个范围有gap锁,则会阻塞当前插入,避免幻读
在rr的事务隔离级别下,insert on duplicate key、replace into与普通insert只插入意向锁和记录锁不同
insert只有存在主键冲突和唯一键冲突时会加 s next-key锁
insert on duplicate key 和replace into sql
如果存在主键上有冲突会插入排它next-key lock,没有冲突正常插入
如果唯一键上冲突会插入排它next-key lock,没有唯一键冲突会加gap锁(mysql5.7.20是这样,不管唯一键是什么类型都会加gap锁或next-key锁)
mysql可以设置以下Mysql参数监控事务加锁情况:
set GLOBAL innodb_status_output_locks=ON; 开启锁监控
set GLOBAL innodb_status_output=ON; 开启innodb标准监控
innodb status log中加锁含义:
记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
间隙锁(LOCK_GAP): lock_mode X locks gap before rec
Next-key 锁(LOCK_ORNIDARY): lock_mode X
插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention
heap no 1 PHYSICAL RECORD supremum 代表当前表中右边界
heap no 0 PHYSICAL RECORD inifimum 代表当前表中左边界
死锁实验:
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`alias` int(11) NOT NULL,
`age` int(11) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `u_idx_alias` (`alias`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8;
- mysql> describe test;
- +-------+---------+------+-----+---------+----------------+
- | Field | Type | Null | Key | Default | Extra |
- +-------+---------+------+-----+---------+----------------+
- | id | int(11) | NO | PRI | NULL | auto_increment |
- | alias | int(11) | NO | UNI | NULL | |
- | age | int(11) | NO | | 0 | |
- +-------+---------+------+-----+---------+----------------+
- 3 rows in set (0.02 sec)
- session1:
- mysql> begin;replace into test values(22,22,11);
- Query OK, 0 rows affected (0.00 sec)
- session2,3执行完后在执行
- mysql> commit;
- Query OK, 0 rows affected (0.01 sec)
- session2:
- mysql> begin;replace into test values(23,23,11);
- Query OK, 0 rows affected (0.00 sec)
- session1 commit后
- Query OK, 1 row affected (12.22 sec)
- session3:
- mysql> begin;replace into test values(24,24,11);
- Query OK, 0 rows affected (0.00 sec)
- session1 commit后
- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
session1未提交前 查看死锁日志show engine innodb status
加锁分析:线程1在插入主键索引的时候,由于无完全匹配记录,故不加锁;在插入记录的时候,无与意向锁不兼容的锁,直接插入主键索引;插入二次索引的时候,因无完全匹配记录,故在supremum记录上加Gap锁。
线程2在插入主键索引的时候,由于无完全匹配记录,故不加锁;在插入记录的时候,无与意向锁不兼容的锁,直接插入主键索引;插入二次索引的时候,因无完全匹配记录,故在supremum记录上加Gap锁,但在插入时因发现supremum记录上已经有其它线程的GAP锁,与自身插入意向锁冲突,故等待。
线程3与线程2类似。
session1提交后 查看死锁日志show engine innodb status;
加锁分析:线程2的插入意向锁等待线程3的Gap,而线程3的插入意向锁等待线程2的插入意向锁,出现死锁。
本地实验批量更新数据为1k,1w,10w下的更新时间
1k
1w
10w
方法二 insert into 批量更新
原理:insert into … on duplicate key update 这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。
缺点: 1、注意,如果有多个列违反了唯一性约束,就会按照索引的顺序,修改跟第一个
索引冲突的行
2、并发高的时候容易造成死锁
缺点1实验:
- mysql> select * from t_insert;
- +-----+-----+-----+
- | id | i1 | i2 |
- +-----+-----+-----+
- | 105 | 101 | 209 |
- | 107 | 102 | 223 |
- | 123 | 103 | 227 |
- | 129 | 104 | 226 |
- +-----+-----+-----+
- 4 rows in set (0.00 sec)
- 插入130,102,202 但是存在102冲突,所以这里只更新107这一行
- mysql> INSERT INTO t_insert ( id, i1, i2 ) VALUES (130,102,202) ON DUPLICATE KEY UPDATE i1 = VALUES ( i1 ), i2 = VALUES ( i2 );
- Query OK, 2 rows affected (0.09 sec)
- mysql> select * from t_insert;
- +-----+-----+-----+
- | id | i1 | i2 |
- +-----+-----+-----+
- | 105 | 101 | 209 |
- | 107 | 102 | 202 |
- | 123 | 103 | 227 |
- | 129 | 104 | 226 |
- +-----+-----+-----+
- 4 rows in set (0.00 sec)
死锁分析:
步骤一:设置Mysql参数,开启加锁输出
set GLOBAL innodb_status_output_locks=ON;
set GLOBAL innodb_status_output=ON;
步骤二:依次执行以下session中sql
session1:
- session1:
- mysql> CREATE TABLE `test` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `alias` int(11) NOT NULL,
- `age` int(11) NOT NULL DEFAULT '0',
- PRIMARY KEY (`id`),
- UNIQUE KEY `u_idx_alias` (`alias`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- mysql> insert into test(alias,age) values(1,1),(7,7),(15,15);
- mysql> begin;insert into test(alias,age) value(12,12) on duplicate key update age=9;
- 等session2,3执行完后,再执行
- mysql> commit;
- session2:
- mysql>begin;insert into test(alias,age) value(13,13) on duplicate key update age=11;
- session3:
- mysql> begin;insert into test(alias,age) value(14,14) on duplicate key update age=11;
- Query OK, 0 rows affected (0.01 sec)
- session1 commit后:
- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
session1 commit前:show engine innodb status
加锁分析:线程1在插入主键索引的时候,由于无完全匹配记录,故不加锁;在插入记录的时候,无与意向锁不兼容的锁,直接插入主键索引;插入二次索引的时候,因无完全匹配记录,故在7-15区间上加Gap锁。
线程2在插入主键索引的时候,由于无完全匹配记录,故不加锁;在插入记录的时候,无与意向锁不兼容的锁,直接插入主键索引;插入二次索引的时候,因无完全匹配记录,故在7-15区间上加Gap锁,但在插入意向锁时因发现7-15区间上已经有其它线程的GAP锁,与自身插入意向锁冲突,故等待。
线程3与线程2类似。
session1 commit后:show engine innodb status
加锁分析:线程1提交后线程2的插入意向锁等待线程3的Gap,而线程3的插入意向锁等待线程2的插入意向锁,故而死锁出现。
解决办法:1、在数据库表中只建立主键,不建立其他唯一索引
2、在有可能有并发事务执行的insert内容一样情况下不使用该语句
3、使用RC级别,RC隔离级别下不会有gap锁
4、升级到没有问题的版本,如8.0.18新版本
本地实验批量更新数据为1k,1w,10w下的更新时间
1k
1w
10w
方法三 临时表 批量更新
原理:创建临时表,先更新临时表,然后从临时表中update
临时表生命周期只存在当前连接,连接关闭临时表会自动清除,不用临时表的时候最好显示删除掉,因为关闭连接/连接池的时候SQL Server才帮我们清除临时表了,但事实上连接池只有当程序重启的时候才清掉。在 binlog_format='row’的时候,临时表的操作不记录到 binlog
缺点:1、需要用户有临时表操作权限
2、不显示清除掉则可能导致tempdb占用磁盘空间非常大
3、设置 innodb_temp_data_file_path 选项,最好设定文件最大上限,超过上限时,需要生成临时表的SQL无法被执行
本地实验批量更新数据为1k,1w,10w下的更新时间
1k
1w
10w
方法四 case when 批量更新
缺点: 1、拼接语句太长,当大于 max_allowed_packet限制时无法执行
2、数据量少时执行速度还可以,多的时候就不行了,可读性差
本地实验批量更新数据为1k,1w,10w下的更新时间
1k
1w
10w
批量更新条数数据对比
方法 |
1K耗时(s)|影响行数 |
1w耗时(s)|影响行数 |
10w耗时(s) |
replace into |
0.216 | 2k |
3.065 | 2w |
7.02 |
insert into on duplicate |
0.148 | 2k |
0.557 | 2w |
6.02 |
临时表更新 |
0.099 | 1k |
0.626 | 1w |
5.063 |
case when |
0.216 | 1k |
10.045 | 1w |
10min54.54 |
总结:
根据实验结果,综合下来是更新速度 临时表 > insert into > replace into > case when
replace into和insert into在并发高的情况下都容易发生死锁,且在表中有多个唯一主键时会发生数据丢失的情况。replace into效率比insert into低的原因是,从操作上replace 比insert就多操作一步,当存在主键或唯一键冲突时,replace 是先删后插入,要重新维护索引,所以速度慢。而insert into是直接更新,更新对主键索引没有影响。所以当更新数据比较多的时候更建议使用insert into批量更新
临时表的使用场景应该是数据集比较小,用完马上清理,会话隔离。根据自己的机器配置,当在更新的数据量占用磁盘空间在可承受范围内的时候可以使用临时表来进行更新。在使用时需要有操作权限,并且需要设置临时文件的上限
case when批量更新数据比较小时可以使用,一旦数据量比较大拼接sql过长,可读性差,执行效率也低
如果想要用replace into和insert into 就需要考虑死锁的发生,将事务隔离级别设置成rc,没有gap锁,可以降低插入时发生死锁的概率,但是在存在唯一键或主键冲突时还是会加next-key锁(并没有实验)。建议像执行频率不高类似定时任务一天执行一次这种业务可以用insert into和replace into
还有一种批量更新的方式就是将n条批量更新语句放在同一事物中,降低事物的开销,合并binlog,redolog,降低刷磁盘的次数(还是要看自己配置的innodb_flush_log_at_trx_commit,和sync_binlog配置)。但事务只是保证acid,对每条sql的语义分析还是要执行n次,不过比执行多条更新语句要快很多