Skip to content

3.30版本,saveBatch批量插入较慢,批量插入300多条数据,需要19s。 #2786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
angiely1115 opened this issue Jul 29, 2020 · 30 comments

Comments

@angiely1115
Copy link

当前使用版本(必填,否则不予处理)

3.3.0

该问题是如何引起的?(确定最新版也有问题再提!!!)

重现步骤(如果有就写完整)

报错信息

@miemieYaho
Copy link
Contributor

来个复现demo

@angiely1115
Copy link
Author

从sql打印的日志来看,是单条插入。只是最后一次性事物提交。单条插入那里最耗时

@huanghuanghui
Copy link

遇到同样的问题,看源码中的实现是一个foreach循环,saveBranch中集合有多少条数据就提交多少次,导致效率非常低

@huanghuanghui
Copy link

使用官网推荐的p6spy进行SQL的日志分析,看到saveBranch方法其实是,就是封装了一个集合提交
image

@angiely1115
Copy link
Author

但是有时候插入3000多条平均1s左右。这跟表的字段大小应该也有关系。我插入300条耗时19s,这张表字段比较多。

@miemieYaho
Copy link
Contributor

是有大字段吧?

@huanghuanghui
Copy link

设置开启数据库批量提交&rewriteBatchedStatements=true,可以解决批量插入慢的问题

url: jdbc:mysql://127.0.0.1:3306/order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true

@angiely1115
Copy link
Author

已设置,不然批量的不会生效

@angiely1115
Copy link
Author

是有大字段吧?

没有大字段,就单独的字段比较多,有40多个。

@huanghuanghui
Copy link

使用mybatis原生的《foreach》标签写下批量插入脚本,单元测试看看执行时间

@ListenBom
Copy link

我也遇到了相同的问题,我使用的是mariadb驱动,mybatis-plus版本为3.3.2,在执行批量操作时会在: protected boolean executeBatch(Collection list, int batchSize, BiConsumer<SqlSession, E> consumer) ;方法内耗时较久,每次插入大概不到100条数据,耗时平均在10S左右。当我使用xml的foreach时插入时间恢复正常。
我的表结构:
CREATE TABLE pd01eh (
id char(32) NOT NULL COMMENT 'ID',
PD01ID char(32) NOT NULL COMMENT '借贷账户信息单元ID',
PD01AI01 varchar(60) DEFAULT NULL COMMENT '账户编号',
PD01ED01 varchar(60) DEFAULT NULL COMMENT '还款状态',
PD01EJ01 varchar(60) DEFAULT NULL COMMENT '逾期(透支)总额',
PD01ER03 varchar(60) DEFAULT NULL COMMENT '月份',
create_time datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='历史表现信息';
其中id为uuid。
我同事在使用相同版本时却没有出现相同的问题

@angiely1115
Copy link
Author

我也遇到了相同的问题,我使用的是mariadb驱动,mybatis-plus版本为3.3.2,在执行批量操作时会在: protected boolean executeBatch(Collection list, int batchSize, BiConsumer<SqlSession, E> consumer) ;方法内耗时较久,每次插入大概不到100条数据,耗时平均在10S左右。当我使用xml的foreach时插入时间恢复正常。
我的表结构:
CREATE TABLE pd01eh (
id char(32) NOT NULL COMMENT 'ID',
PD01ID char(32) NOT NULL COMMENT '借贷账户信息单元ID',
PD01AI01 varchar(60) DEFAULT NULL COMMENT '账户编号',
PD01ED01 varchar(60) DEFAULT NULL COMMENT '还款状态',
PD01EJ01 varchar(60) DEFAULT NULL COMMENT '逾期(透支)总额',
PD01ER03 varchar(60) DEFAULT NULL COMMENT '月份',
create_time datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='历史表现信息';
其中id为uuid。
我同事在使用相同版本时却没有出现相同的问题

请问你数据连接url有设置开启数据库批量提交&rewriteBatchedStatements=true这个吗?

@miemieYaho
Copy link
Contributor

miemieYaho commented Aug 4, 2020

字段越多 ognl需要处理的表达式越多,你40个字段排除id就有39个,那就要至少过39*2 次

@abc136609517
Copy link

我觉得mp批量操作加个flag参数开关 flag=false关闭就使用单条sql执行 最后事务提交 flag=true开启就使用批量的sql操作(非单条) 这样我们在写的时候 可以自由选择 毕竟量少的走批量就行 mp考虑的是量非常大的情况所以使用单条操作 单条操作是每1000条做一次事物提交(也可以指定条数)

@huanghuanghui
Copy link

这块是可以改进的,mybaits-plus使用的刷新SQL Session的方式批量提交,比起数据库批量执行速度相对慢很多。还有批量执行方法,都在方法上添加@transaction注解,量比较大的时候,就会事务超时,事务应该是用户去控制的,而不能为用户去开启事务。

@suhengli
Copy link

设置开启数据库批量提交&rewriteBatchedStatements=true,可以解决批量插入慢的问题

url: jdbc:mysql://127.0.0.1:3306/order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true

我postgresql设置了这个没有效果,他还是一条一条的insert。我用的是 sharding-jdbc-spring-boot-starter 3.1.0.M1

@ListenBom
Copy link

我也遇到了相同的问题,我使用的是mariadb驱动,mybatis-plus版本为3.3.2,在执行批量操作时会在: protected boolean executeBatch(Collection list, int batchSize, BiConsumer<SqlSession, E> consumer) ;方法内耗时较久,每次插入大概不到100条数据,耗时平均在10S左右。当我使用xml的foreach时插入时间恢复正常。
我的表结构:
CREATE TABLE pd01eh (
id char(32) NOT NULL COMMENT 'ID',
PD01ID char(32) NOT NULL COMMENT '借贷账户信息单元ID',
PD01AI01 varchar(60) DEFAULT NULL COMMENT '账户编号',
PD01ED01 varchar(60) DEFAULT NULL COMMENT '还款状态',
PD01EJ01 varchar(60) DEFAULT NULL COMMENT '逾期(透支)总额',
PD01ER03 varchar(60) DEFAULT NULL COMMENT '月份',
create_time datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='历史表现信息';
其中id为uuid。
我同事在使用相同版本时却没有出现相同的问题

请问你数据连接url有设置开启数据库批量提交&rewriteBatchedStatements=true这个吗?

有设置此属性的。如果我没有设置此属性的话,xml插入应该也会比较慢吧?我的DB链接如下:
jdbc:mariadb://127.0.0.1:3306/credit?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true

@dongbaibai
Copy link

继承ServiceImpl,再重新把批量插入的方法重写一遍。。。

@suhengli
Copy link

suhengli commented Sep 9, 2020

继承ServiceImpl,再重新把批量插入的方法重写一遍。。。

谢谢

@dongbaibai
Copy link

刚才仔细的看了一下MP中saveBatch方法,他采用的是BatchExecutor来进行所有statement的提交,你有300多条数据,就会执行300多次的sql
至于Mybatis的xml中使用标签的话,其实是执行的一条sql。所以效率高些
我尝试去修改一下MP批量插入的方式,要是能成功的话我再把代码贴出来

@dongbaibai
Copy link

自定义InsertBatch需要以下三步骤:
【RewriteSqlInjector】自定义SQL注入器
`/**

  • 为了在SQL注入的时候,注入自定义的InsertBatch方法
    */
    @component
    public class RewriteSqlInjector extends DefaultSqlInjector {

    @OverRide
    public List getMethodList(Class<?> mapperClass) {
    return Stream.of(
    new Insert(),
    new Delete(),
    new DeleteByMap(),
    new DeleteById(),
    new DeleteBatchByIds(),
    new Update(),
    new UpdateById(),
    new SelectById(),
    new SelectBatchByIds(),
    new SelectByMap(),
    new SelectOne(),
    new SelectCount(),
    new SelectMaps(),
    new SelectMapsPage(),
    new SelectObjs(),
    new SelectList(),
    new SelectPage(),
    new InsertBatch()
    ).collect(toList());
    }
    }`

【BatchMapper】自定义Mapper基类,作用与BaseMapper相似,需要实现InsertBatch的Mapper需继承该接口

public interface BatchMapper {

int insertBatch(@Param("list") Collection<T> entities);

}

【InsertBatch】自定义批量插入的方法 **
`/

  • SQL注入的格式:
  • CREATE TABLE user (
  • id int NOT NULL AUTO_INCREMENT,
  • name varchar(255) DEFAULT NULL,
  • PRIMARY KEY (id)
  • ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  • 批量生成的SQL
  •  insert into user (id,name) values
    
  •  <foreach collection="list", item="i" separator=",">
    
  •      (#{i.id},#{i.name})
    
  •  </foreach>
    

*/
public class InsertBatch extends AbstractMethod {

private final String SQL = "<script>\nINSERT INTO %s %s VALUES %s\n</script>";

private final String METHOD = "insertBatch";

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    KeyGenerator keyGenerator = new NoKeyGenerator();
    String columnScript = SqlScriptUtils.convertTrim(tableInfo.getKeyInsertSqlColumn(true) + tableInfo.getFieldList().stream().map(TableFieldInfo::getColumn).collect(joining(NEWLINE)),
            LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
    String valuesScript = SqlScriptUtils.convertForeach(LEFT_BRACKET +
            SqlScriptUtils.convertTrim(getAllInsertSqlPropertyMaybeIf("i.", tableInfo), null, null, null, COMMA)
            + RIGHT_BRACKET, "list", null, "i", ",");
    String keyProperty = null;
    String keyColumn = null;
    // 表包含主键处理逻辑,如果不包含主键当普通字段处理
    if (StringUtils.isNotEmpty(tableInfo.getKeyProperty())) {
        if (tableInfo.getIdType() == IdType.AUTO) {
            /** 自增主键 */
            keyGenerator = new Jdbc3KeyGenerator();
            keyProperty = tableInfo.getKeyProperty();
            keyColumn = tableInfo.getKeyColumn();
        } else {
            if (null != tableInfo.getKeySequence()) {
                keyGenerator = TableInfoHelper.genKeyGenerator(tableInfo, builderAssistant, METHOD, languageDriver);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }
    }
    String sql = String.format(SQL, tableInfo.getTableName(), columnScript, valuesScript);
    SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
    return this.addInsertMappedStatement(mapperClass, modelClass, METHOD, sqlSource, keyGenerator, keyProperty, keyColumn);
}

/**
 * 对属性添加prefix,并且不生成<if>标签
 * @param prefix
 * @param tableInfo
 * @return
 */
private String getAllInsertSqlPropertyMaybeIf(final String prefix, TableInfo tableInfo) {
    final String newPrefix = prefix == null ? EMPTY : prefix;
    return tableInfo.getKeyInsertSqlProperty(newPrefix, true) +
            tableInfo.getFieldList().stream().map(i ->
                    i.getInsertSqlProperty(newPrefix)).filter(Objects::nonNull).collect(joining(NEWLINE));
}

}
`

@angiely1115
Copy link
Author

已经使用,谢谢

@dongbaibai
Copy link

你是怎么解决的大兄弟

@angiely1115
Copy link
Author

你是怎么解决的大兄弟

就是按照你给的方式处理的呀

@dongbaibai
Copy link

好吧,我以为你有其他的方法
这个能解决你的问题吗

@angiely1115
Copy link
Author

好吧,我以为你有其他的方法
这个能解决你的问题吗

可以,其实官方的也不是慢,主要是我的这种表太大,表字段过多。

@javaApprenticeHzk
Copy link

MySQL的JDBC连接的url中要加rewriteBatchedStatements参数,并保证5.1.13以上版本的驱动,才能实现高性能的批量插入。
MySQL JDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySQL数据库,批量插入实际上是单条插入,直接造成较低的性能。
只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL
另外这个选项对INSERT/UPDATE/DELETE都有效。
原先10万条数据25个字段8个大字节字段10秒完成

@Nymph2333
Copy link

MySQL的JDBC连接的url中要加rewriteBatchedStatements参数,并保证5.1.13以上版本的驱动,才能实现高性能的批量插入。
MySQL JDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySQL数据库,批量插入实际上是单条插入,直接造成较低的性能。
只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL
另外这个选项对INSERT/UPDATE/DELETE都有效。
原先10万条数据25个字段8个大字节字段10秒完成

之前在CSDN上看说是只对Insert生效,并且是大于3条的时候生效

@libra1010
Copy link

BatchMapper

最新版使用此方式后,一直提示
nested exception is org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [list, param1] nested exception is org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [list, param1]
找了很久也没有找到原因

@Kang-Yang
Copy link

最新版本,加上参数不生效,还是单条去执行

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests