写点什么

全表遍历并处理数据有点慢?放开!我来!

用户头像
林一
关注
发布于: 28 分钟前
全表遍历并处理数据有点慢?放开!我来!

场景模拟

假设你的公司现在有 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 的数据总不能一下子查出来吧?行吧。那就分页,代码如下:


@Testpublic 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@Transactionalpublic 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.htmlResultSet 这一栏。那我们在 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 的查询导出,以及相关查询推送等,具体业务可以自己再进一步分析。

用户头像

林一

关注

冲~ 2020.02.13 加入

还未添加个人简介

评论

发布
暂无评论
全表遍历并处理数据有点慢?放开!我来!