记一次Mock100万数据到数据库遇到的问题
2016-07-03 14:20
218 查看
背景
需要Mock100万数据到数据库。逐条插入
如果一条一条的插入,模拟1000条,花费了8秒,100万的话,大约花费3个小时,太慢。批量插入
问题出现
1000条一次进入插入。consoleLogger.info("This Batch, {}, Time start : {}", batchTotal, s); orderMapper.batchInsert(orderList); consoleLogger.info("This Batch, {}, Time elapsed : {}", batchTotal, System.currentTimeMillis() - s);
按照理论,应该比逐条快很多,但是,结果花费了14秒,非常奇怪,开始查找原因。
求助Google
看到两篇有价值的帖子:http://www.cnblogs.com/aicro/p/3851434.html
http://blog.jobbole.com/85657/
试着改成100条一个批次,很神奇,1000条数据插入,10个循环,总计60毫秒就完成了。难道用MyBatis操作MySQL数据库的批量操作,对于10条一批和1000条一批,性能差别这么大?
不可理解,本着根本上解决问题的思路,继续追根溯源。
加Log
<logger name="org.mybatis.spring" additivity="true" level="${logger_level}"> <appender-ref ref="serviceAppender" /> </logger> <logger name="org.apache.mybatis" additivity="true" level="${logger_level}"> <appender-ref ref="serviceAppender" /> </logger>
打出一些日志,但是没有参考价值。
Debug
于是Debug,一直跟下去,跟了很久,终于在这里发现问题。public static String showSql(Configuration configuration, BoundSql boundSql) { String sql = boundSql.getSql().replaceAll("[\\s]+", " "); try{ Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings.size() > 0 && parameterObject != null) { TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { sql = sql.replaceFirst("\\?", getParameterValue(parameterObject)); } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { Object obj = metaObject.getValue(propertyName); sql = sql.replaceFirst("\\?", getParameterValue(obj)); } else if (boundSql.hasAdditionalParameter(propertyName)) { Object obj = boundSql.getAdditionalParameter(propertyName); sql = sql.replaceFirst("\\?", getParameterValue(obj)); } } } } }catch (Exception e){ logger.error("showSql invoke error:{}",e); } return sql; }
原来数据库的批量插入操作,很快就完成返回了,然后,我们自己工程中写的慢查询拦截器,对于超过300毫秒,会有拦截处理,把?替换成参数值,大约到Log里面,这个过程非常慢。
于是临时注释掉这个代码,测试了一下,批量插入非常快了,并且是批量插入的行数越多,越快。
为什么批量快
批量执行效率高的主要原因是合并后日志量(MySQL的binlog和innodb的事务)减少了,降低日志刷盘的数据量和频率,从而提高效率。通过合并SQL语句,同时也能减少SQL语句解析的次数,减少网络传输的IO。
为什么这个方法慢
String是Immutable,因为对于批量 parameterMappings很多,几万个,于是反复进行String.replaceFirst,每次执行这个操作,都会复制一份新的字符串,于是内存复制了几万个次,于是就很慢了。解决办法,用Parttern和Match,这种方式,在内存只会产生一个备份,然后就会从头到尾替换下去,经测试,性能果然提高很多,不再是14秒,只是几百毫秒。
public static String showSql(Configuration configuration, BoundSql boundSql) { String sql = boundSql.getSql().replaceAll("[\\s]+", " "); StringBuffer sqlBuilder = new StringBuffer(); try{ Pattern pattern = Pattern.compile("\\?"); Matcher matcher = pattern.matcher(sql); Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings.size() > 0 && parameterObject != null) { TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { if(matcher.find()) { matcher.appendReplacement(sqlBuilder, getParameterValue(parameterObject)); } } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { Object obj = metaObject.getValue(propertyName); if(matcher.find()) { matcher.appendReplacement(sqlBuilder, getParameterValue(obj)); } } else if (boundSql.hasAdditionalParameter(propertyName)) { Object obj = boundSql.getAdditionalParameter(propertyName); if(matcher.find()) { matcher.appendReplacement(sqlBuilder, getParameterValue(obj)); } } } } } matcher.appendTail(sqlBuilder); }catch (Exception e){ logger.error("showSql invoke error:{}",e); } return sqlBuilder.toString(); }
然后,进一步,又通过对于某些不想被拦截的SQL,头部增加/*NoMonitor*/的方式,避免进行字符串处理。
于是问题彻底解决,插入100万数据,10000条一个批次,大约需要6分钟。
如果不是MySQL呢
向所有的持久化存储插入数据,批量都会比逐条快得多。我过去试过的Solr和Redis,大约是几十倍的差别。
其他如ES、Mongo之类,应该也差不多,有空可以写代码试验一下。
相关文章推荐
- MySQL中的integer 数据类型
- MySQL存储过程
- mysql中int、bigint、smallint 和 tinyint的区别与长度
- mysql load data 导出、导入 csv
- source命令执行SQL脚本文件
- MySQL创建用户及权限控制
- MySQL管理数据表
- linux下mysql添加用户
- mysql procedure
- mysql触发器
- MySQL 备份和恢复策略
- mac下安装mysql(转载)
- mysql 修改编码 Linux/Mac/Unix/通用(杜绝修改后无法启动的情况!)
- MySQL数据的导出、导入(mysql内部命令:mysqldump、mysql)
- mysql数据行转列
- Linux下修改MySQL编码的方法
- MySQL Server 日志
- MySQL 安全事宜
- MySQL 备份与恢复