为什么Mybatis批量模式不生效?

前两天看一个项目测试比较慢的问题。看日志的时候发现某个请求执行插入语句数百次,耗时数秒,据了解是用的Mybatis的批量插入方式,但我印象中批量模式是每秒数以千记的,差距巨大。所以去看了一下实际代码,批量模式的写法片段是这样的:

SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
Method method = mapper.getClass().getDeclaredMethod(methodName);
int count = 0;
for (Object obj : data) {
    method.invoke(mapper, obj);
    if (++count == BATCH_SIZE) {
        sqlSession.flushStatements();
        count = 0;
    }
}
sqlSession.flushStatements();

这是一个封装过的方法,传入具体的mapper对象,然后通过反射来处理。这个写法和外边一些例子是有些差异的,外边更多是sqlSession.getMapper(X.class)来获取的。

我猜这里可能不对,于是写了一个测试代码,把实现改成getMapper,果然快了,一千条也就不到200毫秒。

看来这里是必须通过getMapper来获取的,修改也很简单了,主要两点优化:

  • 修改传入mapper对象为getMapper调用用于提升性能
  • 修改反射调用为方法引用(例如BiConsumer)用于提升健壮性

然后我花了半小时把Mybatis相关的源码翻了一遍,理清一下getMapper和注入的mapper究竟有什么差别。

源码回顾

阅读源码要搞清楚几个问题:

  • getMapper()和注入的mapper究竟有什么差别?
  • mapper是怎么被注入到Spring Bean去掉?

getMapper()和注入的mapper究竟有什么差别?

通过查看openSession这个方法,可以很容易知道,SqlSession的默认实现DefaultSqlSession和ExecutorType的关系是这样的(SqlSession内部有个Executor,对于ExecutorType是BATCH的情况,这是一个BatchExecutor实例):

DefaultSqlSession --  Executor(BatchExecutor) -- ExecutorType.BATCH

而通过getMapper这个方法一层层看下去,在MapperProxyFactory可以看到生成的mapper其实是一个代理对象,它会持有SqlSession实例。

protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

这样我们可以理解为,mapper持有SqlSession,而SqlSession持有Executor,采用ExecutorType.BATCH还是ExecutorType.SIMPLE就是在这里区分的,所以要批量插入就不能使用注入的mapper对象。

mapper是怎么被注入到spring Bean去掉?

这个得从SpringBoot中找mybatis的加载位置,从starter和autoconfigure的包找spring.factories.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

可以看到MybatisAutoConfiguration应该是关键入口,找到里边找扫描mapper的地方。调用链路如下:

AutoConfiguredMapperScannerRegistrar#registerBeanDefinitions(实现ImportBeanDefinitionRegistrar接口) => MapperScannerConfigurer#postProcessBeanDefinitionRegistry(实现BeanDefinitionRegistryPostProcessor接口) => ClassPathMapperScanner#doScan真正扫描Mapper Bean => 这里会对对每个类生成一个MapperFactoryBean,它是个FactoryBean。

既然是Factory,我们可以查看getObject,这就是实际返回的对象了。

  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

所以这里的注入的是默认的sqlSession。

MapperFactoryBean除了getObject方法,它还继承DaoSupport,这里有个afterPropertiesSet方法,那么就会在初始化Bean的时候调用,具体实现是检查checkDaoConfig,在这里实现了加入Configuration#mapperRegistry(保存所有的mapper)。

protected void checkDaoConfig() {
   ....
   Configuration configuration = getSqlSession().getConfiguration();
   if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
       ...
       configuration.addMapper(this.mapperInterface);
       ...
   }
}

于是乎整个链路就通了,启动的时候扫描Mapper,添加到Spring Bean里边,同时放入Configuration#mapperRegistry。无论是普通注入的Mapper,还是getMapper获取到的,都是和Configuration#mapperRegistry关联起来的,只是内部的Executor是不同的。

批量插入的反模式

后来和一些同学讨论了一下这个案例,还提到了常见批量插入的写法。不过这些写法,在我看来是一些反模式,这里也顺便记录一下。

批量模式本质上是使用Statement#addBatchStatement#executeBatch来完成的,具体可以看BatchExecutor的实现。至于为什么快,主要是减少了通讯次数,而不是靠SQL技巧。

我也发现很多项目并不使用批量模式,而是通过一些特殊的SQL技巧。例如MySQL的insert是支持插入多行的,但oracle的没有这个特性,于是见到这种写法:

INSERT ALL
  INTO mytable (...) VALUES (...)
  INTO mytable (...) VALUES (...)
SELECT * FROM dual

又或者

INSERT INTO mytable 
SELECT 'c1''c2''c3' FROM dual
UNION ALL
SELECT 'c1''c2''c3' FROM dual;

这是通过Mybatis的动态SQL拼接出来,但是总体来说,这种做法后患无穷。主要理由是:

  • 性能和批量模式比,没有明显优势
  • SQL会随着插入行数变化而变化,对于数据库来说都是不同的预编译SQL
  • 拼接的配置变长,相当于把逻辑放到SQL中去,写出来的SQL容易和特定数据库强绑定
  • 如果操作的数量多,SQL过大,一次性消耗过高,同样还是需要拆分

所以在实际应用中,建议还是多使用批量模式,而不拼接SQL方式。


原文始发于微信公众号(程序员的胡思乱想):为什么Mybatis批量模式不生效?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/22603.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!