前言
在上一篇文章《通用,高效,可控的海量数据导出组件-设计篇》中,我们讨论了组件的完整设计思路,知道建模+队列+阻塞是实现组件的三个关键点,在本篇文章中,我们将从代码层面介绍组件的具体实现方式,挑选其中的关键代码进行解释,文末有该组件代码的开源地址,欢迎一起讨论
导出任务定义
如果导出任务是同步处理的,就没必要持久化了。但如果导出任务是异步处理的,就必须将导出任务的信息持久化,等到机器空闲资源充足时才能基于这些信息处理导出任务,我们将导出任务的信息存到数据库中,具体定义如下:
CREATE TABLE `easy_export_task` (
`id` bigint NOT NULL AUTO_INCREMENT,
`status` int NOT NULL DEFAULT '0' COMMENT '任务状态,0:等待中,1:已取消,2:已完成,3:运行中,4:失败',
`db_function_method` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`db_function_params` varchar(2048) COLLATE utf8mb4_general_ci DEFAULT NULL,
`db_results_process` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '数据库返回结果加工逻辑',
`ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`mtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`file_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '导出文件名',
`download_url_processor_class_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '异步下载URL处理类',
`download_url_processor_params` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '异步下载URL处理类额外入参',
`trigger_executed_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '触发执行时间(重复触发时记录最新触发时间)',
`finished_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '完成时间(包括成功,失败,取消)',
`upload_class_name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '上传至文件服务器处理类',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
status:导出任务状态,在代码中对应枚举类org.huge.data.enums.ExportTaskStatusEnum
WAITING(0, "等待中"),
// 暂无使用, 保留
CANCEL(1, "已取消"),
SUCCESS(2, "已完成"),
RUNNING(3, "运行中"),
FAILED(4, "失败");
状态之间的流转过程示意图如下:
当导出任务持久化到数据库时,状态为WAITING;当导出任务被某台机器处理时,状态置为RUNNING;当机器处理完导出任务后,将该任务的状态置为SUCCESS;如果导出任务在处理过程中发生异常,则状态置为FAILED;处理FAILD状态的导出任务可以置为WAITING,重新进入队列中等待被处理;对于处于WAITING状态的导出任务,可以置为CANCEL,处于CANCEL状态的导出任务将永远不会被处理
处于WAITING状态的导出任务会排成一个先进先出的队列,机器从队头依次取出导出任务进行处理,排序字段为ctime
db_function_method,db_function_params,db_results_process,file_name,download_url_processor_class_name,download_url_processor_params,upload_class_name都是导出任务的详细信息,依次是查询数据逻辑的类名,查询数据的入参,加工数据逻辑的类名,导出文件名,对于文件下载链接后的处理逻辑的类名,对于文件下载链接后的处理逻辑的入参,文件上传至文件服务器逻辑的类名
db_function_method是一个泛型接口
@FunctionalInterfacepublic interface DBQueryFunction <T, R>{ List<R> query(T t);}
db_function_params是这个接口的入参,即其中的T,R就是从数据库返回的查询的数据。这个接口要实现的功能是:从数据库中查询要导出的基本数据,查询过程必须使用Mybatis(这涉及到后面流式查询的实现)
db_results_process是一个泛型接口
@FunctionalInterfacepublic interface DBProcessFunction<T, R> { R process(T t);}
其中的入参T是db_function_method的返参,即:数据库返回的查询的数据,R是加工后的数据。这个接口要实现的功能是:对数据查询的数据进行加工,返回符合导出要求的数据。这个接口的入参只是数据库查询数据中的一条,例如:从数据库查询的数据是100条,则这个接口会被调用100次。但有时候,我们希望批量加工数据,举例:加工逻辑里需要调用第三方接口参数补充数据,并且这个第三方接口支持批量查询,这时,每条数据再单独查询就不太合适,因此db_result_process也可以是另一个泛型接口
@FunctionalInterfacepublic interface DBBatchProcessFunction <T, R>{ R process(T t);}
这个泛型接口与前者的不同点是,这个的入参T,是多条数据库返回数据,是一个List,返参R,也是一个List。例如:从数据库查询的数据是100条,则这个接口可能被调用10次,每次传入10条数据加工。用户可以实现DBProcessFunction接口也可以实现DBBatchProcessFunction接口
upload_class_name是一个接口
public interface AsyncUploadS3 { String upload(String fileName, Workbook workbook);}
第一个入参是文件名,即:file_name,第二个入参是导出数据的内存对象,在这个接口里需要实现如下功能:将workbook上传至自已的文件服务器上,并返回该文件的下载地址
download_url_processor_class_name是一个接口
public interface DownloadUrlProcessor { void manageDownUrl(String downloadUrl, Map<String, String> manageDownloadUrlParams);}
其第一个入参是文件服务器返回的下载地址,即AsyncUploadS3的返参,第二个入参是一个Map对象,如果处理过程中需要额外的参数,可以通过这个参数传入,即:导出任务信息中的download_url_processor_params。在这个接口里要实现的功能是,对于文件下载URL的处理过程,常见的有:将URL发送到下载人的邮箱,这时下载人的信息可以参数第二个入参Map传入
这些接口的执行顺序如图所示:
接收请求后的同步处理流程
应用服务器接收到请求后的处理流程如图所示:
提交导出任务入口是这个方法:
org.huge.data.service.OrcaExportEntryService#submitExportTask
这个方法有重载,其中一个支持传入DBBatchProcessFunction,其中一个不支持,根据实际情况选择即可
a. 判断是否是重复请求:这里采用Redis实现,其中的key默认采用:查询类名+查询参数+加工类名,也可以通过submitExportTask的入参criticalHitComposeKey和criticalHitComposeKeyParam来自定义key的组件,其中,criticalHitComposeKey是一个泛型接口,入参是criticalHitComposeKeyParam。当判断为重复请求时,则抛出组件的自定义异常OrcaExportException。如果不是重复请求,则进入下一步
b. 判断是否可以执行同步查询:满足同步查询需要满足三个条件,第一是允许任务以同步方式处理,第二是导出数量低于设定阈值,第三是当前机器正在执行的同步导出任务的数据低于设定阈值
第一个条件,是一个配置信息
// 是否使用同步导出 0:不使用 1:使用
private int abandonSyncExport = 1;
第二个条件,因为DBQueryFunction接口里就是查询数据库,因此,我们基于Mybatis拦截器修改SQL,将其从select xxx from yyy改成select count(*) from yyy。Mybatis拦截器的逻辑在
org.huge.data.interceptors.ExportTotalCountInterceptor
第三个条件,使用信号量实现,当信号量获取成功,则按同步方式处理当前导出任务
public static final Semaphore SYNC_EXPORT_MAX_COUNT = new Semaphore(10);
如果上述条件不满足,就要转为异步导出,此时,需要将导出任务的信息持久化到数据库中,转由异步处理,进入步骤d。如果条件满足了,则进行同步处理,进入下一步
c. 同步处理导出任务:同时处理的流程就是按前面方法的执行流程图依次执行,由于是同步到处,生成的文件不用上传到文件服务器上,因为AsyncUploadS3和DownloadUrlProcessor不需要执行,而是操作HTTP流返回下载文件,可以直接使用EasyExcel提供的封装接口
d. 异步处理导出任务:此时导出任务的信息已经持久化到数据库中,后端返回前端成功,前端告知用户导出任务异步处理中,导出任务的异步处理在下面单起一个标题讲
导出任务异步处理
a. 异步导出任务线程池:与同步导出每台机器通用信息号限制同一时刻处理的导出任务数量类似。同一时刻,一台机器处理异步导出任务的数量也要限制,我们通用线程池来实现这个功能
异步任务线程池必须在Web应用启动时就构建完毕,因此我们把它写到类的static块中初始化异步任务线程池的相关配置
b. 导出任务的异步处理方式:对于要以异步方式处理的导出任务,它们均在数据库中存储着,每台机器会去数据库取出一定数量的任务来处理,在导出任务状态的流转图可以看到,机器去数据库获取导出任务有两个触发时间,一是事件触发,一是时间触发。事件触发是指A机器收到前端发过来的导出任务,但因不满足同步导出条件,因此需要异步处理,这时A先将导出任务放到数据库中进行排队,再从队头取出要处理的导出任务。时间触发是指每台机器内部有个定时任务,定时扫描数据库中是否有未处理完的导出任务,避免任务积压。通过两种触发机制,导出任务就会平均分摊给每台机器处理。定时任务的实现我们采用java.util.concurrent.ScheduledThreadPoolExecutor实现,它也是一个线程池,需要在web应用启动时初始化好,为了避免多台机器从数据库取导出任务的时间一致,定时任务的启动时间添加一个随机数
需要注意的是,机器在从数据库取出导出任务处理时,需要加行锁,并且将导出任务的状态修改为处理中,避免同一个导出任务同时被多台机器处理
c. 异步导出任务处理:每个异步导出任务的处理的核心代码在org.huge.data.service.handler.AsyncExportTask这个类里。第一步是对导出任务的参数解析,要特别说明的是,我们定义接口时用的是泛型接口,这里我用了Spring对于泛型的处理类ResolvableType来解析泛型的具体参数。第二步就是建立用于写的线程池,用于转的线程池,读的操作就放在当前线程,不再单独新建线程,这样读,转,写就是完全独立并行执行的。第三步是要建立三个步骤之间存储数据的中转站,我们用java.util.concurrent.BlockingQueue来实现,只需使用一个作为存储执行转操作后的数据,因为另外两个数据中转站是通用HTTP缓冲区,及Apache POI的rowAccessWindowSize实现的
d. 将普通查询改为流式查询:DBQueryFunction里的代码会通过Mybatis操作数据库,但使用的是普通查询,我们通过Mybatis拦截器将其改成流式查询,这样只需查询一次数据库即可,通过两个拦截器实现org.huge.data.interceptors.PureQueryParamsStoreInterceptor,org.huge.data.interceptors.StreamQueryCloseStatementInterceptor
e. 转操作与写操作间的交互:写操作会不断从BlockingQueue中获取数据写入到文件当中,需要有一个标记告许写操作已经没有数据了
f. 收尾:任务处理完后更新导出任务的状态
其它细节可以下载代码查看,组件github地址: github.com/jiabushou/o…
文章总结
介绍了导出组件中一些代码的实现原理,并开源了组件的源代码,在下一篇,《通用,高效,可控的海量数据导出组件--效果篇》中,将比较组件导出方案与其它方案在不同指标上的差异
文章修改记录
2024-11-23 首次发表