记一次Mock100万数据到数据库遇到的问题

来源:互联网 发布:php的就业前景好 编辑:程序博客网 时间:2024/04/30 04:00


背景


需要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之类,应该也差不多,有空可以写代码试验一下。

0 0
原创粉丝点击