java mybatis 性能问题_源码分析 Mybatis 的 foreach 为什么会出现性能问题

背景

最近在做一个类似于综合报表之类的东西,需要查询所有的记录(数据库记录有限制),大概有1W条记录,该报表需要三个表的数据,也就是根据这 1W 个 ID 去执行查询三次数据库,其中,有一条查询 SQL 是自己写,其他两条是根据别人提供的接口进行查询,刚开始的时候,没有多想,直接使用 in 进行查询,使用 Mybatis 的 foreach 语句;项目中使用的是 jsonrpc 来请求数据,在测试的时候,发现老是请求不到数据,日志抛出的是 jsonrpc 超时异常,继续查看日志发现,是被阻塞在上面的三条SQL查询中。

在以前分析 Mybatis 的源码的时候,了解到,Mybatis 的 foreach 会有性能问题,所以改了下 SQL,直接在代码中拼接SQL,然后在 Mybatis 中直接使用 # 来获取,替换 class 测试了下,果然一下子就能查询出数据。

前提

这里先不考虑使用 in 好不好,如何去优化 in,如何使用 exists 或 inner join 进行代替等,这里就只是考虑使用了 in 语句,且使用了 Mybatis 的 foreach 语句进行优化,其实 foreach 的优化很简单,就是把 in 后面的语句在代码里面拼接好,在配置文件中直接通过 #{xxx} 或 ${xxx} 当作字符串直接使用即可。

测试

在分析 foreach 源码之前,先构造个数据来看看它们的区别有多大。

建表语句:

713179fef799c8cb7bc62416887f56a5.png

插入 1W 条数据:

a301d6a20b971fa6645f5c9ca4dfac30.png

POJO 类:

dbd3cc2640cb297bbb74dee62765cb03.png

方式一

通过原始的方式,使用 foreach 语句:

1. 在 dao 里面定义方法:

9ed56241d8d9bba891fcf6d32aaa7955.png

2. 配置文件SQL:

52169aa2e8d7e7c95b0859cdb46a4da1.png

3. 执行 main 方法:

0110e1834cb36f69811aeca8cd6fd8fc.png

可以看到通过 foreach 的方法,大概需要 3s

方式二

在代码中封装 SQL ,在配置文件中 通过 ${xxx} 来获取:

1. 在 dao 添加方法:

ad9e220f6007b93b15c7a54999676314.png

2. 配置文件SQL:

0347d07e934d42c31115a6e369a54287.png

3. 执行 main 方法:

5d5e23bdfd503cfd8115d22c3832f174.png

通过拼接 SQL,使用 ${xxx} 的方式,执行同样的 SQL ,耗时大概 360 ms

方式三

在代码中封装 SQL ,在配置文件中 通过 #{xxx} 来获取:

1. 在 dao 中添加方法:

669326a9e396f0f095ead824e03ee27a.png

2. 配置文件SQL:

b72cd42acbf10cc44ff49300cadafdf7.png

3. 执行 main 方法:

03cb12c3acae8384c4ff2132f0a78151.png

通过拼接 SQL,使用 #{xxx} 的方式,执行同样的 SQL ,耗时大概 30 ms

总结

通过上面三种方式可以看到,使用不同的方式,耗时的差别还是麻大的,最快的是 拼接 SQL,使用 #{xxx} 当作字符串处理,最慢的是 foreach。为什么 foreach 会慢那么多呢,后面再分析源码的时候再进行分析;而这里同样是拼接 SQL 的方式,#{xxx} 和 ${xxx} 耗时却相差 10 倍左右;我们知道,Mybatis 在解析 # 和 $ 这两种不同的符号时,采用不同的处理策略;使用过 JDBC 的都知道,通过 JDBC 执行 SQL 有两种方式: Statment 对象和PreparedStatment 对象, PreparedStatment 表示预编译的SQL,包含的SQL已经预编译过了,SQL 中的参数部分使用 ?进行占位,之后使用 setXXX 进行赋值,当使用 Statement 对象时,每次执行一个SQL命令时,都会对它进行解析和编译。所有 PreparedStatment 效率要高一些。那么 Mybatis 在解析 # 和 $ 的时候,分别对应的是这两种对象,# 被解析成 PreparedStatment 对象,通过 ? 进行占位,之后再赋值,而 $ 被解析成 Statement ,通过直接拼接SQL的方式赋值,所以,为什么同样是通过在代码中拼接 SQL ,# 和 $ 的耗时不同的原因。

PS:上面只是介绍了三种方式,应该没有人问,拼接SQL为 (1,2,3,4,5),在配置SQL中通过 #{xxx} 来获取吧

foreach 源码解析

下面来看下 foreach 是如何被解析的,最终解析的 SQL 是什么样的:

在 Mybatis 中,foreach 属于动态标签的一种,也是最智能的其中一种,Mybatis 每个动态标签都有对应的类来进行解析,而 foreach 主要是由 ForEachSqlNode 负责解析。

ForeachSqlNode 主要是用来解析 节点的,先来看看 节点的用法:

ddf4bdeb5f84200ed8e9687372488317.png

最终被 数据库执行的 SQL 为 select * from person where 1=1 and id in (1,2,3,4,5)

先来看看它的两个内部类:

PrefixedContext

该类主要是用来处理前缀,比如 "(" 等。

516f8ee68e9354e1c14ab459d8e8976f.png

FilteredDynamicContext

FilteredDynamicContext 是用来处理 #{} 占位符的,但是并未绑定参数,只是把 #{item} 转换为 #{_frch_item_1} 之类的占位符。

2d78b97d76957d2b062a99ed7a02990e.png

f3397a93c84cf187efb8e28bb5d15cd7.png

ForeachSqlNode

了解了 ForeachSqlNode 它的两个内部类之后,再来看看它的实现:

7f405cd6ef3a409dc8bbfecc3e8140fd.png

3bbe3546e49990171bbdadd854fa9031.png

9e9813267e80a758939cb9cc4772c271.png

f4598a56fd74a6bdc1aa0bf8a6bc56cf.png

所以该例子:

7d0022b8a3650c50f5f6019db6c440bb.png

解析之后的 SQL 为:

select * from person where 1=1 and id in (#{__frch_item_0}, #{__frch_item_1}, #{__frch_item_2}, #{__frch_item_3}, #{__frch_item_4})

之后再通过 PreparedStatment 的 setXXX 来进行赋值。

所以,到这里,知道了 Mybatis 在解析 foreach 的时候,最后还是解析成了#的方式,但是为什么还是很慢呢,这是因为需要循环解析 #{__frch_item_0} 之类的占位符,foreach 的集合越大,解析越慢。既然知道了需要解析占位符,为何不自己拼接呢,所以就可以在代码中拼接好,而不再使用 foreach 啦。

所以,Mybatis 在解析 foreach 的时候,底层还是会解析成 #号的形式而不是 $的形式,既然知道了这个,如果 需要 foreach 的集合很大,就可以使用代码拼接 SQL ,使用(#{xxx}) 的方式进行获取,不要再拼接成 (1,2,3,4,5) 再使用 ${xxx}的方式啦。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值