··

springboot + mybaits 对数据库进行百万级别大数据量读取,并对这些数据进行操作,应用能顶得住吗?

背景

某问答机器人中,有大数据记录查询的需求,原先在程序中限制了查询时间,为避免oom,查询时间限制间隔非常短。这样的结果显然不能满足实际需求中的数据导出场景,实际场景可能需要导出一个月,一个季度甚至一年的数据
这个问题的解决方案不是立马就有思路的。一段时间后的某天晚上洗澡的时候突然以前看过的一篇文章从脑海里一闪而过。流加载,一个以前看到的时候觉得并没有卵用但是又被作者称之为王炸的技术。进行原理学习后,决定将这个思路进行实践。

试验

普通加载

在测试数据库中造出180万7321条测试数据。

SELECT * FROM 表名;

编写程序将其全部读出

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_大数据

过一段时间后,程序宕机

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_后端_02

使用的堆达到将近2000M

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_spring boot_03

流加载

同样的sql与逻辑,使用流加载

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_java_04

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_spring boot_05

程序成功执行完毕,使用的堆峰值将近1000M,可以看到对堆的消耗降低了几乎一倍

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_后端_06

实践

试验效果还是比较明显,约了一天晚上,发版至正式环境,进行实践。

在进行大数据量(97.8万)真实生产数据的读取操作时,实际执行区间数据库指标正常(无明显波动,整体平缓)

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_java_07


不过整体微服务cpu使用率存在尖峰(35%左右),但指标仍算正常(因为逻辑中还涉及到excel写入的操作,这部分cpu的消耗不能完全归属于流加载这个行为)

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_spring boot_08


程序成功执行,返回结果,生成的excel,结果条数:978306

原理

  1. 客户端与mysql server端建立连接
  2. mysql server通过输出流将sql查询结果集输出至本地内核对应的socket buffer
  3. 内核数据通过tcp链路回传至客户端的服务器内核缓冲区
  4. jvm逐条从服务器的内核缓冲区中读取数据
  5. Mybaits cursor 流加载底层代码流程
    a. 从sql结果集中取出一条数据放入objectWrapperResultHandler中
    b. 遍历时,通过handlerResult逐条取出上下文结果,并释放上下文(context.stop())
    c. 重复流程直到sql结果集中无结果,结果集关闭
    分享一篇介绍得比较好的文章:

总结

流式读取可以作为主流读取方案吗?我认为不行。
        流加载在读取时,需要一直维持数据库链接,是一种占用链接的操作。流加载因为是逐条读取的,所以效率上,比我们平时的分页加载、一次性加载效率低。所以在使用流加载时,一定要注意并发数和加载时长的控制。流加载在我看来是一种类似于以时间换空间的手段。
那么流加载在实际中没有用武之地?当然不是。
        实际上,在后管场景下,并发量不高,且经常会做大数据的导出或处理。流加载我相信会成为一个妙招。
        并发可以通过加锁的方式,限制并发;加载时长可以通过对查询条件的限制去进行限制,进行流加载前,可以count一下即将查出的数据总数,总数小于某个阈值时,才会进入流加载逻辑。对查询时间也可根据实际场景去做限制。
        应用所处环境,可以容纳怎样的并发?可以接受怎样的时长?需要以试验为基础去决定。

流加载写法

只要你所在应用采用的orm层框架是MyBaits。那么就可以以这种方式进行流加载改造。
指定返回值为 Cursor 类型,MyBatis 就明白这个查询方法是一个流式查询

@Select({
            "sql略"
    })
    Cursor<Map<String, Object>> getByIdNo(String idNo);

流加载需要维持一个数据库长链接,因此可以这样保持链接一直打开,执行完方法后关闭链接

try (
  SqlSession sqlSession = sqlSessionFactory.openSession();
  Cursor<Map<String, Object>> cursor = sqlSession.getMapper(XXXMapper.class).getByIdNo(idNo)
        ) {
            writer.write(cursor, false);
        }

不论是对现有方法的改造,还是新方法的开发,都是非常方便的。

真正的王炸?

在我的实践中,流加载在excel表格生成上,也许真的可以成为一个王炸。

hutool中包装了一个excel生成工具,支持大数据生成,且该工具主要就为了解决内存溢出的问题

https://hutool.cn/docs/#/poi/Excel%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%94%9F%E6%88%90-BigExcelWriter

Springboot double返回给前端变成了科学计数法 springboot返回数据量大_大数据_09


在我们生成excel的时候,为安全性考虑,会将生成的excel加密,将加密操作整合上流加载生成excel逻辑中,代码如下:

// excel写入
ByteArrayOutputStream ops = new ByteArrayOutputStream();
ExcelWriter writer = ExcelUtil.getBigWriter();
try (
      SqlSession sqlSession = sqlSessionFactory.openSession();
      Cursor<Map<String, Object>> cursor = sqlSession.getMapper(XXXMapper.class).getByIdNo(idNo)
            ) {
                writer.write(cursor, false);
            }
writer.flush(ops);
writer.close();
writer.flush(ops);
writer.close();

// excel加密
byte[] b;
ByteArrayInputStream inputStream = new ByteArrayInputStream(ops.toByteArray());
b = FileUtil.encryptExcel(inputStream, "123456");