java異步導(dǎo)出的實現(xiàn)過程
背景
假設(shè)我們有一個在線學(xué)習(xí)平臺,管理員需要定期生成銷售報告,包括課程銷售情況和用戶購買情況等重要數(shù)據(jù)。然而,由于數(shù)據(jù)量較大,生成報告可能需要較長時間,并且可能會占用大量系統(tǒng)資源,從而影響用戶的使用體驗。為了解決這個問題,我們考慮采用異步導(dǎo)出的方案。
異步導(dǎo)出的工作原理是將導(dǎo)出操作放在一個異步任務(wù)中執(zhí)行,而不是立即在用戶發(fā)起導(dǎo)出請求后執(zhí)行導(dǎo)出操作。這樣一來,用戶無需等待導(dǎo)出任務(wù)完成,就可以繼續(xù)進(jìn)行其他操作,而系統(tǒng)則在后臺完成導(dǎo)出任務(wù)。
這種方案有以下優(yōu)點:
- 提高系統(tǒng)響應(yīng)速度: 用戶發(fā)起導(dǎo)出請求后,系統(tǒng)可以立即響應(yīng)而不必等待導(dǎo)出任務(wù)完成,從而提高了系統(tǒng)的響應(yīng)速度。
- 改善用戶體驗: 用戶無需等待導(dǎo)出任務(wù)完成,可以繼續(xù)使用系統(tǒng)進(jìn)行其他操作,這有助于提升用戶體驗。
- 降低系統(tǒng)負(fù)載: 將耗時的導(dǎo)出操作放在異步任務(wù)中執(zhí)行,可以避免阻塞系統(tǒng)資源,從而降低系統(tǒng)的負(fù)載,確保其他用戶的操作不受影響。
異步導(dǎo)出在許多需要處理大量數(shù)據(jù)或耗時操作的場景中都非常有用,可以有效提升系統(tǒng)的性能和用戶體驗。
數(shù)據(jù)庫設(shè)計
首先我們需要設(shè)計一個保存導(dǎo)出任務(wù)的表,需要記錄流轉(zhuǎn)狀態(tài)、操作人、任務(wù)參數(shù),后續(xù)任務(wù)的創(chuàng)建、導(dǎo)出完成/失敗都需要操作這張表
CREATE TABLE `t_export_task`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`task_id` varchar(50) NOT NULL COMMENT '任務(wù)id',
`task_type` tinyint(4) NOT NULL COMMENT '任務(wù)類型',
`task_param` varchar(1000) NOT NULL COMMENT '任務(wù)參數(shù)',
`status` tinyint(3) NOT NULL DEFAULT 0 COMMENT '狀態(tài) 0-處理中 1-成功 -1失敗',
`file_url` varchar(500) DEFAULT NULL COMMENT '文件url',
`remark` varchar(200) DEFAULT NULL COMMENT '備注',
`create_user_id` int(11) NOT NULL COMMENT '操作人id',
`create_user_name` varchar(50) NOT NULL COMMENT '操作人名稱',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='導(dǎo)出任務(wù)記錄';代碼實現(xiàn)
導(dǎo)出工具類 :
負(fù)責(zé)提交導(dǎo)出任務(wù)、取消任務(wù)以及上傳導(dǎo)出文件到OSS服務(wù)器等功能。
- 導(dǎo)出任務(wù)線程池: 通過
ExecutorService線程池來執(zhí)行導(dǎo)出任務(wù),并確保線程池的單例化,防止重復(fù)創(chuàng)建,提高性能。 - 導(dǎo)出類型對應(yīng)的任務(wù): 使用
Map<Integer, CompletableFuture<String>>來保存正在運行的導(dǎo)出任務(wù),以便后續(xù)取消任務(wù)或跟蹤任務(wù)狀態(tài)。 - 提交導(dǎo)出任務(wù): 提交導(dǎo)出任務(wù)時,先初始化線程池,然后使用
CompletableFuture.supplyAsync()方法執(zhí)行異步任務(wù),并在異步任務(wù)中生成導(dǎo)出文件,然后上傳到OSS服務(wù)器,最后返回導(dǎo)出文件的URL。 - 取消任務(wù): 取消任務(wù)時,從保存的任務(wù)映射中獲取對應(yīng)的
CompletableFuture實例,并調(diào)用cancel()方法取消任務(wù)。 - 上傳文件到OSS服務(wù)器:
uploadToOSS()方法負(fù)責(zé)實際的文件上傳邏輯,將導(dǎo)出的文件上傳到OSS服務(wù)器,并返回文件的URL
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ExportTaskUtil {
/**
* 導(dǎo)出任務(wù)線程池
*/
private static volatile ExecutorService executorService;
/**
* 導(dǎo)出類型對應(yīng)的任務(wù)
*/
private final Map<Integer, CompletableFuture<String>> runningTasks = Maps.newConcurrentMap();
private final ExportTaskHandlerFactory exportTaskHandlerFactory;
private final FileUploadService fileUploadService;
/**
* @description 提交導(dǎo)出任務(wù)
* @author youmu
* @date 2024/1/26 17:58
* @param exportTask 導(dǎo)出任務(wù)
*/
public CompletableFuture<String> submit(ExportTask exportTask) {
// 初始化線程池
initThreadPool();
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
File exportFile = null;
// 獲取handler
ExportTaskHandler handler = exportTaskHandlerFactory.getHandler(exportTask.getTaskType());
// 生成文件
try {
exportFile = handler.generateExportFile(exportTask.getTaskParam());
if (exportFile == null) {
throw new BizException(CodeEnum.NOT_FOUND, "導(dǎo)出文件為空");
}
// 上傳文件到OSS服務(wù)器,獲取文件URL
return uploadToOSS(exportFile);
} catch (BizException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (exportFile != null) {
FileUtil.del(exportFile);
}
}
},executorService);
runningTasks.put(exportTask.getTaskType(), future);
return future;
}
private static void initThreadPool() {
if (executorService == null) {
synchronized (ExportTaskUtil.class) {
if (executorService == null) {
executorService = ThreadUtil.newFixedExecutor(4, "asyncExport", false);;
}
}
}
}
/**
* @description 取消任務(wù)
* @author youmu
* @date 2024/1/26 19:04
* @param exportTask 任務(wù)
*/
public void cancel(ExportTask exportTask) {
CompletableFuture<String> future = runningTasks.get(exportTask.getTaskType());
if (future != null && !future.isDone()) {
future.cancel(true);
}
}
/**
* @description 上傳文件到OSS服務(wù)器
* @author youmu
* @date 2024/1/29 16:56
*/
private String uploadToOSS(File exportFile) {
// 實現(xiàn)文件上傳邏輯,返回文件URL
return fileUploadService.uploadFileBySize(exportFile,"export/" + exportFile.getName());
}
}
導(dǎo)出任務(wù)處理的工廠類以及相關(guān)的接口和枚舉定義
導(dǎo)出任務(wù)采用來工廠+策略的設(shè)計模式,工廠模式將對象的創(chuàng)建邏輯封裝到工廠類中,策略模式將不同的行為封裝到不同的策略類中,使得代碼具有良好的可擴展性、靈活性和可維護(hù)性。
- ExportTaskHandlerFactory: 這是一個工廠類,用于根據(jù)導(dǎo)出任務(wù)類型獲取對應(yīng)的任務(wù)處理器。在初始化時,它會將所有實現(xiàn)了
ExportTaskHandler接口的處理器注入進(jìn)來,并根據(jù)任務(wù)類型建立起映射關(guān)系。 - ExportTaskHandler 接口: 這是一個導(dǎo)出任務(wù)處理器的接口,定義了生成導(dǎo)出文件和獲取導(dǎo)出任務(wù)類型的方法,具體的導(dǎo)出任務(wù)處理器需要實現(xiàn)該接口。
- ExportTaskTypeEnum 枚舉: 這是一個枚舉類型,定義了導(dǎo)出任務(wù)的類型,包括了任務(wù)類型的代碼和描述信息。
- OrderExportHandler 類: 這是一個具體的導(dǎo)出任務(wù)處理器的實現(xiàn)類,用于處理訂單導(dǎo)出任務(wù)。它實現(xiàn)了
ExportTaskHandler接口,根據(jù)具體業(yè)務(wù)邏輯生成導(dǎo)出文件,并提供了獲取任務(wù)類型的方法。
/**
* @description ExportTaskHandler 工廠類
* @author youmu
* @date 2024/1/26 18:04
*/
@Slf4j
@Component
public class ExportTaskHandlerFactory {
private final Map<Integer, ExportTaskHandler> handlerMap = Maps.newHashMap();
@Autowired
public ExportTaskHandlerFactory(List<ExportTaskHandler> handlers) {
for (ExportTaskHandler taskHandler : handlers) {
handlerMap.put(taskHandler.getExportType().getCode(), taskHandler);
}
}
public ExportTaskHandler getHandler(Integer exportType) {
return handlerMap.get(exportType);
}
}
/**
* @description ExportTaskHandler
* @author youmu
* @date 2024/1/26 18:06
*/
public interface ExportTaskHandler {
File generateExportFile(String param) throws Exception;
ExportTaskTypeEnum getExportType();
}
/**
* @description 導(dǎo)出任務(wù)類型
* @author youmu
* @date 2024/1/29 11:01
*/
@AllArgsConstructor
@Getter
public enum ExportTaskTypeEnum implements IEnum<Integer, String> {
CROWD_PACKAGE(1, "人群包"),
;
private final Integer code;
private final String message;
}
public class OrderExportHandler implements ExportTaskHandler{
@Override
public File generateExportFile(String param) throws Exception {
return null;
}
@Override
public ExportTaskTypeEnum getExportType() {
return null;
}
}業(yè)務(wù)調(diào)用
導(dǎo)出任務(wù)的門面類 ExportTaskFacade,它提供了一系列方法來提交、取消、重試導(dǎo)出任務(wù),并提供了查詢導(dǎo)出任務(wù)的分頁接口。
- 提交任務(wù)(submitTask): 提交導(dǎo)出任務(wù)時,根據(jù)是否傳入
taskId參數(shù)來判斷是新建任務(wù)還是更新任務(wù)。如果是新建任務(wù),則創(chuàng)建一個新的ExportTask實例并保存到數(shù)據(jù)庫中,然后調(diào)用doSubmit方法提交任務(wù);如果是更新任務(wù),則更新任務(wù)的狀態(tài)為正在處理,并調(diào)用doSubmit方法提交任務(wù)。 - 任務(wù)提交處理(doSubmit): 使用
exportTaskUtil.submit(exportTask)提交異步導(dǎo)出任務(wù),并定義了任務(wù)完成后的處理邏輯。如果任務(wù)執(zhí)行成功,則更新任務(wù)狀態(tài)為成功,并設(shè)置文件的URL;如果任務(wù)執(zhí)行失敗,則記錄失敗日志,并更新任務(wù)狀態(tài)為失敗,同時記錄異常信息。 - 取消任務(wù)(cancelTask): 根據(jù)傳入的
taskId獲取對應(yīng)的導(dǎo)出任務(wù),然后調(diào)用exportTaskUtil.cancel(exportTask)取消任務(wù)。 - 重試任務(wù)(retryTask): 根據(jù)傳入的
taskId獲取對應(yīng)的導(dǎo)出任務(wù),先取消任務(wù)以防止異常情況,然后重新提交任務(wù)。
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ExportTaskFacade {
private final ExportTaskService exportTaskService;
private final UserService userService;
private final ExportTaskUtil exportTaskUtil;
public void submitTask(Integer exportType, String param) {
submitTask(null,exportType,param);
}
public void cancelTask(Long taskId) {
ExportTask exportTask = exportTaskService.getById(taskId);
AssertUtils.notNull(exportTask, new BizException(CodeEnum.NOT_FOUND,"導(dǎo)出任務(wù)不存在"));
exportTaskUtil.cancel(exportTask);
}
public void retryTask(Long taskId) {
ExportTask exportTask = exportTaskService.getById(taskId);
AssertUtils.notNull(exportTask, new BizException(CodeEnum.NOT_FOUND,"導(dǎo)出任務(wù)不存在"));
// 取消任務(wù),防止異常情況還在執(zhí)行
exportTaskUtil.cancel(exportTask);
// 提交任務(wù)
submitTask(taskId,exportTask.getTaskType(),exportTask.getTaskParam());
}
private void submitTask(Long taskId, Integer exportType, String param) {
ExportTask exportTask;
if(taskId == null) {
// 保存導(dǎo)出任務(wù)
exportTask = new ExportTask();
Integer userId = AuthInfoHolder.getUserId();
exportTask.setTaskId(CodeGenUtil.genCode(GenCodeTypeEnum.DL));
exportTask.setCreateUserId(userId);
exportTask.setCreateUserName(userService.findById(userId).getUserName());
exportTask.setTaskType(exportType);
exportTask.setTaskParam(param);
exportTaskService.save(exportTask);
} else {
// 更新導(dǎo)出任務(wù)
exportTask = exportTaskService.getById(taskId);
exportTaskService.lambdaUpdate()
.eq(ExportTask::getId, exportTask.getId())
.set(ExportTask::getStatus, ExportStatusEnum.PROCESSING.getCode())
.update();
}
doSubmit(exportTask);
}
private void doSubmit(ExportTask exportTask) {
exportTaskUtil.submit(exportTask).thenAccept(url->{
exportTaskService.lambdaUpdate()
.eq(ExportTask::getId, exportTask.getId())
.set(ExportTask::getStatus, ExportStatusEnum.SUCCESS.getCode())
.update();
}).exceptionally(ex->{
log.error("[導(dǎo)出任務(wù)]執(zhí)行失敗,{}", exportTask.getTaskId(),ex);
exportTaskService.lambdaUpdate()
.eq(ExportTask::getId, exportTask.getId())
.set(ExportTask::getStatus, ExportStatusEnum.FAILURE.getCode())
.set(ExportTask::getRemark, ex instanceof BizException ? ex.getMessage() : "未知異常")
.update();
return null;
});
}
public Page<ExportTaskVO> findPage(ExportTaskRequest request) {
Page<ExportTask> page = exportTaskService.findPage(request);
List<ExportTaskVO> voList = ConverterUtil.toVO(ExportTaskConverter.class, page.getRecords());
Page<ExportTaskVO> pageVO = new Page<>();
pageVO.setTotal(page.getTotal());
pageVO.setSize(page.getSize());
pageVO.setCurrent(page.getCurrent());
pageVO.setPages(page.getPages());
pageVO.setRecords(voList);
return pageVO;
}
}流程圖

總結(jié)
過以上實踐,我們成功實現(xiàn)了一個輕量級的異步導(dǎo)出方案,具有以下優(yōu)點:
- 使用線程池管理異步任務(wù),確保了任務(wù)的并發(fā)執(zhí)行和資源的合理利用。
- 采用 CompletableFuture 實現(xiàn)異步導(dǎo)出和回調(diào)更新,簡化了異步任務(wù)的編寫和管理。
- 使用工廠模式和策略模式實現(xiàn)導(dǎo)出任務(wù)處理器,使得系統(tǒng)具有良好的可擴展性和靈活性。
然而,這種方案也存在一些缺點:
- 資源管理不足: 如果異步導(dǎo)出任務(wù)的并發(fā)量過大,而線程池的資源配置不足,則可能導(dǎo)致任務(wù)排隊等待執(zhí)行,影響任務(wù)的實時性和響應(yīng)速度。
- 任務(wù)執(zhí)行效率低下: 如果導(dǎo)出任務(wù)的處理時間過長,且線程池的工作線程數(shù)量有限,則可能導(dǎo)致任務(wù)執(zhí)行效率低下,無法及時完成任務(wù),影響系統(tǒng)的整體性能。
- 可靠性不高,無法保證任務(wù)一定會被執(zhí)行或執(zhí)行成功,特別是在系統(tǒng)故障或異常情況下。
針對這些缺點,可以考慮以下優(yōu)化方案:
- 合理調(diào)整線程池配置: 根據(jù)系統(tǒng)的實際負(fù)載情況和性能需求,合理配置線程池的大小和工作線程數(shù)量,確保資源的有效利用和任務(wù)的及時執(zhí)行。
- 優(yōu)化任務(wù)處理邏輯: 對任務(wù)的處理邏輯進(jìn)行優(yōu)化,盡量減少任務(wù)的執(zhí)行時間和資源消耗,提高任務(wù)的執(zhí)行效率和響應(yīng)速度。
- 引入異步消息處理機制: 使用消息隊列或事件驅(qū)動模型來實現(xiàn)任務(wù)的異步處理,進(jìn)一步解耦任務(wù)提交和任務(wù)執(zhí)行過程,提高系統(tǒng)的可擴展性和靈活性。
- 引入定時任務(wù)調(diào)度器: 使用定時任務(wù)調(diào)度器(如 xxl-job)來定期掃描和重試執(zhí)行異常任務(wù)。當(dāng)任務(wù)執(zhí)行時間超過一定閾值(如2小時)或者任務(wù)執(zhí)行異常時,自動觸發(fā)重試機制,保證任務(wù)的及時執(zhí)行。
- 增加任務(wù)監(jiān)控和告警機制: 實時監(jiān)控任務(wù)的執(zhí)行情況,當(dāng)發(fā)現(xiàn)任務(wù)執(zhí)行異常或超時時,及時發(fā)送告警通知,以便運維人員及時處理和修復(fù)。
通過以上優(yōu)化方案,可以提高異步導(dǎo)出方案的可靠性和穩(wěn)定性,確保任務(wù)能夠及時執(zhí)行并完成,同時降低了系統(tǒng)的維護(hù)成本和風(fēng)險。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
idea插件之mybatis log plugin控制臺sql的問題
這篇文章主要介紹了idea插件之mybatis log plugin控制臺sql,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
詳解Java的MyBatis框架中的緩存與緩存的使用改進(jìn)
很多人在使用MyBatis的緩存后經(jīng)常會遇到MySQL分頁查詢的顯示問題,針對于此,這里我們就來詳解Java的MyBatis框架中的緩存與緩存的使用改進(jìn),首先來回顧一下MyBatis的緩存機制與執(zhí)行:2016-06-06
idea+spring?boot創(chuàng)建項目的搭建全過程
Spring?Boot是Spring社區(qū)發(fā)布的一個開源項目,旨在幫助開發(fā)者快速并且更簡單的構(gòu)建項目,這篇文章主要介紹了idea+spring?boot創(chuàng)建項目的搭建全過程,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-09-09
IntelliJ IDEA 統(tǒng)一設(shè)置編碼為utf-8編碼的實現(xiàn)
這篇文章主要介紹了IntelliJ IDEA 統(tǒng)一設(shè)置編碼為utf-8編碼的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2020-06-06
RocketMQ源碼解析topic創(chuàng)建機制詳解
這篇文章主要為大家介紹了RocketMQ源碼解析topic創(chuàng)建機制詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
idea設(shè)置回車換行縮進(jìn)空格數(shù)為4的實現(xiàn)方式
文章主要講述了解決在設(shè)置里回車換行僅縮進(jìn)兩空格的問題,通過將源代碼設(shè)置從回車兩空格改為回車四空格,解決了作者習(xí)慣上的不適2026-04-04
Spring?Boot小型項目如何使用異步任務(wù)管理器實現(xiàn)不同業(yè)務(wù)間的解耦
這篇文章主要介紹了Spring?Boot小型項目如何使用異步任務(wù)管理器實現(xiàn)不同業(yè)務(wù)間的解耦,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-08-08

