通用,高效,可控的海量数据导出组件-设计篇

113 阅读8分钟

 前言

在上一篇文章《通用,高效,可控的海量数据导出组件-调研篇》中,我们讨论了数据导出过程中每一步的注意事项及现有文章的解决方式,并提出设计一个通用,高效,可控的数据导出组件。在这一篇文章中,我们将介绍这个组件的完整设计

设计思想

为了实现通用这个目标,我们需要对数据导出过程建模,将数据导出过程拆解成多个子过程。因为有了具体的模型后,导出过程就统一了,就能写成一个通用的组件

为了实现高效这个目标,我们使用流水线的设计思路(导出过程建模也是使用流水线设计思路的前提),导出过程中数据在环节1处理完后,交给环节2继续处理,此时,环节1是空闲的,可以处理下一条数据,这样比起整条数据在所有环节处理再处理下一条,资源的利用效率更高,整体数据的处理时长更短

为了实现可控这个目标,我们引入阻塞队列,我们可以在前述流水线的每个环节之间放一个队列,不同环节要处理数据都从队列中获取,数据处理完要交给下一个环节也是先放队列当中,由下一个环节从队列中取出处理。队列具有阻塞性质,具体而言,当队列满的时候,如果此时往队列写入数据,会阻塞住,直至队列有空闲位置;当队列空的时间,如果此时往队列读取数据,会阻塞住,直至队列中有具体数据。基于这个性质,我们就可以通过调整队列的大小来对各环节进行控制,例如:当前机器资源占用较多,就不能让各环节处理的数据过多,我们就减小队列的大小;反之,则增加队列的大小

由上可知,建模+队列+阻塞 是组件设计的三个核心关键点

详细设计

组件设计完整示意图

数据导出过程建模

使用流水线的前提是整个过程可以划分为多个独立的子过程,每个独立的子过程就是流水线上一个独立的执行单元

数据导出模型=读+转+写

读:指数据从数据库读取到JVM内存

转:指数据库的数据加工为符合导出要求的数据

写:指符合导出要求的数据进行持久化

我们假设导出的每条数据的处理过程都是相互独立的,没有相互依赖,则基于流水线的思路,一条数据在处于”转“的过程中时,”读“可以读取另一条数据,相类似的,当一个数据处理”写“的过程中时,另一条数据可以进行”转“的过程,导出过程与导出数据间的关系如图所示:

集群视角下导出任务处理设计

每个机器收到导出请求后,判断采用哪种处理方式,有两种处理方式:同步处理/异步处理

同步处理:

a. 判断导出数据量与所设阈值关系:同步处理意味着要返回浏览器数据,浏览器也会等待回应,但网关会设置超时时间,因此,可以根据超时时间预估一个处理数据量阈值,当要导出的数据超过阈值,使用异步处理;当要导出的数据未超过阈值,则进行下一步判断

b. 判断当前机器正在处理的导出请求个数:设置一个信号量,每次机器要同步处理某个导出请求时,必须先获取信号量,如果获取成功,则使用同步处理;如果获取失败,则使用异步处理

c. 同步处理导出请求:处理完导出请求后,释放信号量,此时从数据库按请求时间由近到远取出未处理的导出请求继续处理

异步处理:

a. 导出请求持久化至队列中:队列是先进先出队列,用数据库实现,按插入时间排序即可,数据库需要记录导出请求的信息,如:请求参数,是否已处理等,这一步,是将导出请求”推“送到队列中

b. 定时任务处理积压导出请求:每台机器都会设置一个定时任务,每隔一段时间从数据库按请求时间由近到远取出未处理的导出请求处理,为了防止同一个导出请求同时被多台机器处理,从数据库查询时必须加行锁,将导出请求取到内存中后,将导出请求标记为处理中,查询与更新操作必须在一个事务里,数据处理完后,更新数据库中这条导出请求的处理状态。由上可知,被机器A接收的导出请求,可能被B由定时任务处理,这一步,是将导出请求从队列中”拉“取

相关技术知识点:信号量,定时任务,事务

单机视角下导出任务处理设计

读:MySQL服务器返回SQL对应的数据时,数据需要依次经过MySQL服务器的HTTP缓冲区,应用服务器的HTTP缓冲区,应用服务器的JVM内存。使用单独的线程执行”读“操作,我们让应用服务器以流式查询方法向MySQL服务器发起查询请求,此时,应用服务器会立即获得一个迭代器,每当应用服务器利用迭代器迭代一次,MySQL服务器就会返回一条数据,剩余的数据会阻塞在HTTP缓冲区中,应用服务器每读取到一条数据后,就放入阻塞队列中(这个阻塞队列专门放置从MySQL读取的数据,下称阻塞队列1),如果阻塞队列1已经满了,"读"操作就会在此环节阻塞住,就不会继续从MySQL服务器中获取数据,直至队列有空闲位置

转:使用单独的线程执行"转"操作,应用服务器从阻塞队列1取出待加工的数据,如果阻塞队列1为空,则"转"操作就会在此环节阻塞住,直至阻塞队列1中有数据。对于取出来的数据,由于数据间间是独立的,可以使用多线程处理取出来的数据,处理后的数据放到另一个阻塞队列中(这个阻塞队列专门放置已经加工后的,符合导出要求的数据,下称阻塞队列2),如果阻塞队列2已经满了,”写“操作就会在此环节阻塞住,就不会继续从阻塞队列1中获取数据,直到阻塞队列2为有空闲位置

写:使用单独的线程执行”写“操作,应用服务器从阻塞队列2取出符合导出要求的数据,如果阻塞队列2为空,则”写“操作就会在此环节阻塞住,直至阻塞队列2中有数据。对于取出来的数据,我们可以使用Apache POI提供的写入接口,先设置滑动窗口的数量,保证内存中常驻的数据条数,然后调用接口将从阻塞队列2取出的数据一条条写入,这里的写入是写入到磁盘当中,但实际应用中,大多数是需要上传到文件服务器的,上传至文件服务器这一步可以交由用户实现

由上可知,同样是阻塞队列,也有不同的实现形式,可以是Java实现,可以是HTTP缓冲区实现,可以是Apache poi的滑动窗口(注:滑动窗口并不具备阻塞性质,所以严格意义上,这个分类不太准确,不过,这里了解其中的设计思路即可)实现。另外,对于由Java实现的阻塞队列1与阻塞队列2,因为同时存在往队列写与从队列中读的操作,还需要注意并发控制

相关技术知识点:多线程,阻塞队列,流式查询,流处理,并发控制

文章总结

在本篇文章中,我们介绍了整个导出组件的设计思路,在下一篇文章《通用,高效,可控的海量数据导出组件-代码篇》中,将介绍关键代码的实现,并开源这个组件

文章内容修改记录

2024-11-09 第一次文章撰写