场景模拟
假设你的公司现在有 10W 用户,现在产品过来要求你进行写个程序进行全员推送。
ok,没问题!咋们先找到咋们要推送的表 account,表结构如下:
CREATE TABLE account (
id int(11) NOT NULL AUTO_INCREMENT,
account varchar(256) NOT NULL COMMENT '账号',
type int(1) NOT NULL COMMENT '账号类型,10:QQ',
time_create bigint(20) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (id),
KEY idx_type (type) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1000352771 DEFAULT CHARSET=utf8
来,先确定下用户数 select count(*) from account;:
接下来咋办呢?那肯定是要查出来然后一个个推送,10W 的数据总不能一下子查出来吧?行吧。那就分页,代码如下:
@Test
public void pagePush() {
long t2 = System.currentTimeMillis();
List<String> pageResult = Lists.newArrayList();
//这里有10W的用户,我就循环1000次来模拟好了,每次取100个人
for (int i = 0; i < 1000; i++) {
Pageable pageable = PageRequest.of(i, 100);
Page<Account> page = accountRepository.findAll(pageable);
page.getContent().stream().forEach(x -> pageResult.add(notice(x)));
}
System.out.println("page时间:" + (System.currentTimeMillis() - t2) + ",推送成功size:" + pageResult.size());
}
private String notice(Account account) {
//进行推送,这里返回推送成功的账号
return account.getAccount();
}
复制代码
测试结果如下:
可以看到使用 page 方式推送完成所需要的时间为 16s 左右。这里的耗时跟多次分页查询和 limit 查询随着偏移量增加性能下降有关!那行吧~既然多次查询会耗时,那我有没有办法直接 select * from account 进行查询呢?普通的返回值返回 List<Account>肯定是不行的了,因为 10W 数据量比较大,不够存。那我试试 Spring Data 1.8 中的流功能,给它返回一个 Stream<Account>试试能不能一条条数据给我呢。来改代码如下:
@Query(value = "select t from Account t")
Stream<Account> findAllUserAccount();
@Test
@Transactional
public void streamPush() {
long t1 = System.currentTimeMillis();
Stream<Account> allUserAccount = accountRepository.findAllUserAccount();
List<String> streamResult = allUserAccount.map(this::notice).collect(Collectors.toList());
System.out.println("Stream时间:" + (System.currentTimeMillis() - t1) + ",size:" + streamResult.size());
}
复制代码
特意将内存改为 -Xmx50M -Xms50M,然后启动了程序,果然被现实狠狠的打脸:
是的,它报错了,并且测试代码似乎没有终止,发生了假死,多次尝试有时候启动会报堆内存不足。说明
它和返回 List<Account>没啥区别。翻阅资料查询有没有办法让它一条一条就返回呢,查阅文档,发现在一般情况下 Mysql 的 ResultSet 被完全检索并存储在内存中。
上面也给出了解决方案,具体可以参考:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-implementation-notes.html的 ResultSet 这一栏。那我们在 jpa 中如何实现呢?
解决方案
要解决这个问题,在 JPA 中可以使用 @QueryHints 来解决,代码如下:
@QueryHints(value = @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE, value = "" + Integer.MIN_VALUE))
@Query(value = "select t from Account t")
Stream<Account> findAllUserAccount();
复制代码
下面修改一下测试代码来验证是否可以一条条返回:
@Test
@Transactional(readOnly = true)
public void streamPush() {
long t1 = System.currentTimeMillis();
Stream<Account> allUserAccount = accountRepository.findAllUserAccount();
List<String> streamResult = allUserAccount.map(this::notice).collect(Collectors.toList());
System.out.println("Stream时间:" + (System.currentTimeMillis() - t1) + ",size:" + streamResult.size());
}
private String notice(Account account) {
//如果多次输出,说明结果提前返回了
System.out.println("进入notice");
//进行推送,这里返回推送成功的账号
return account.getAccount();
}
复制代码
运行接口如下:
可以看到进入 notice 被输出多次,并且整体运行时间缩短到了 1.6s。但是这个时间是包括 System.out.println("进入 notice");耗时的。下面把这句去掉进行测试:
可以发现执行完 10W 条的结果只需要 600ms,相比于原本的 16s 快了 26 倍。
总结
通过 JPA 的 Stream 加上 @QueryHints 可以极大的加快查询的速度,这个场景可以试用于一些 excel 的查询导出,以及相关查询推送等,具体业务可以自己再进一步分析。
评论