MyBatis Plus
作为 MyBatis
的增强版为单表提供了丰富的读写操作,可极大降低代码工程量。
而今天,就让我们一并深入了解如何在 MyBatis Plus
中最大化批量操作性能。
下面就以具体的工程示例让介绍具体的批量操作调优。
一、项目示例
1. 数据构建
在具体的代码开始前首先准备测试表,建表脚本如下:
CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`address` varchar(100) DEFAULT NULL,
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
);
完成建表之后,按照上述数据结构模拟生成构建测试数据。
逻辑并不复杂这里不展开描述,代码如下:
private List<List<User>> mockData(Integer size) {
// 构造测试数据
LocalDateTime now = LocalDateTime.now();
List<User> users = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
User user = new User();
user.setName("name-" + i);
user.setAge(i);
user.setEmail("email-" + i);
user.setAddress("address-" + i);
user.setCreateBy(0L);
user.setCreateTime(now);
users.add(user);
}
// 按 500 拆分多批次
return partition(users, 500);
}
2. 批量保存
那么以批量保存为例,一并来看如何在 Mybatis Plus
中如何实现数据插入。
这里略去其余无关配置项,直入主题可以看到 MP
在 service
父级封装类中提供了 saveBatch()
批量保存方法。
同时,在操作批量插入时为了更优的性能常分批进行保存提交,至于其缘由后续再进行阐述。
public class UserController {
private final UserService userService;
@GetMapping("insert1")
public void insert1(Integer size) {
List<List<User>> lists = mockData(size);
long l = System.currentTimeMillis();
for (List<User> batch : lists) {
userService.saveBatch(batch);
}
log.info("insert1 time: {} ms", System.currentTimeMillis() - l);
}
}
3. 原生插入
同理,利用 SQL
中默认的 insert values
语法,则可在原生的 MyBatis
中利用 foreach
语法在 xml
中手动编写语句从而实现批量插入。
<insert id="insertBatch" keyProperty="id" useGeneratedKeys="true">
insert into user(name, age, email, address, create_by, create_time)
values
<foreach collection="entities" item="entity" separator=",">
(#{entity.name}, #{entity.age}, #{entity.email}, #{entity.address}, #{entity.createBy}, #{entity.createTime})
</foreach>
</insert>
基于上述原生 SQL
语法,其对应的批量数据保存示例如下,同样采取了分批提交的方式。
public class UserController {
private final UserDao userDao;
@GetMapping("insert2")
public void insert1(Integer size) {
List<List<User>> lists = mockData(size);
long l = System.currentTimeMillis();
for (List<User> batch : lists) {
userDao.insertBatch(batch);
}
log.info("insert2 time: {} ms", System.currentTimeMillis() - l);
}
}
4. 性能分析
基于上述的两个示例,以 1w
条数据为例分别执行两个代码块,日志统计如下所示。
从日志结果可以看到 MP
中的批量操作相较于原生的批量操作更为耗时。
com.example.controller.UserController : insert1 time: 3135 ms
com.example.controller.UserController : insert2 time: 607 ms
继续扩大测试集,以 10w
条记录插入为例,从日志可以看到 MP
中的批量耗时仍远超于原生操作。
com.example.controller.UserController : insert1 time: 22871 ms
com.example.controller.UserController : insert2 time: 5927 ms
二、机制剖析
1. 源码解读
那么,造成这一现象的原因又是什么呢?
查看 MP
中的 saveBatch()
实现代码可以看到,其批量操作的核心在于开启长会话 sqlSession()
,在会话中遍历数据执行插入等数据操作,当达到批次大小时通过 flushStatements()
将本批次的数据提交至数据库。
基于此模式下,与数据库的网络请求交互则可大大下降。以 1w
条数据为例且批次大小为 1k
为例,与数据库的网络 IO
则从 1w
次下降至 10
次,所带来的提供不言而喻。
public static <E> boolean executeBatch(SqlSessionFactory sqlSessionFactory, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);
return !CollectionUtils.isEmpty(list) && executeBatch(sqlSessionFactory, log, (sqlSession) -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for(Iterator var7 = list.iterator(); var7.hasNext(); ++i) {
E element = var7.next();
consumer.accept(sqlSession, element);
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
}
});
}
看到这你也许有个疑问,既然降低了网络开销那相较于原生 SQL
为什么仍无法取得相同的性能表现。
这其中的差异之处则是由于数据库自身的实现所带来的,虽然在 MP
中虽然通过长会话节省了网络开销,但数据库层面接受到的仍然是独立的 n
条插入语句,仍需要逐条执行。
而在原生的 SQL
中通过拼接后数据库所接收到的语句数则为 记录数/批量数
。因此,随着数据的递增二者的差异将更为明显,故在大批量的数据操作中更推荐原生的 SQL
方式。
2. 批量更新
了解二者背后的机制后,我们知道在大批量数据下 MP 自带的批量并非最优解。
而在批量插入中则可利用 insert values
实现,批量删除同样可以通过预处理后 in
条件实现。那么,针对批量更新则应当如何处理呢?
虽然在 MySQL
等数据库中针对更新没有类似的 insert values
的语法,但提供了取巧的方式支持 SQL
合并提供,通过在连接信息中添加 allowMultiQueries=true
参数允许多语句提交。
jdbc:mysql://127.0.0.1:3306/database?allowMultiQueries=true
那么,即可在 xml
中通过 foreach
标签拼接合并更新语句从而实现性能最大化。
<update id="updateBatch" parameterType="java.util.List" >
<foreach collection="list" item="item" open="" close="" separator=";">
update user
<set>
<if test="item.name != null" >
name = #{item.name},
</if>
<if test="item.age != null" >
age = #{item.age},
</if>
<if test="item.email != null" >
email = #{item.email},
</if>
<if test="item.address != null" >
address = #{item.address},
</if>
<if test="item.createBy != null" >
create_by = #{item.createBy},
</if>
<if test="item.createTime != null" >
create_time = #{item.createTime}
</if>
</set>
where id = #{item.id}
</foreach>
</update>