前两天看一个项目测试比较慢的问题。看日志的时候发现某个请求执行插入语句数百次,耗时数秒,据了解是用的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#addBatch
和Statement#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