前言
异步任务是处理耗时操作的常用方案。很多用户体验问题都是因为异步任务设计不当:导出等半天没反应、不知道进度、失败了也不知道。这篇给你3种异步任务设计方案。
一、3种异步任务方案对比
| 方案 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 轮询 | 定时查询任务状态 | 简单场景 | 实现简单 | 浪费资源 |
| WebSocket | 服务端主动推送 | 实时性要求高 | 实时 | 复杂度高 |
| 回调 | 任务完成后回调接口 | 第三方服务 | 解耦 | 需要公网地址 |
二、轮询方案
PRD写法
功能:数据导出
异步方案:轮询
流程:
1. 用户点击"导出"
2. 后端创建导出任务,返回task_id
3. 前端每3秒调用接口查询任务状态
4. 任务完成后,显示下载链接
5. 用户点击下载
接口:查询任务状态
请求:GET /api/tasks/{task_id}
响应:
{
"status": "processing", // 状态:pending/processing/success/failed
"progress": 50, // 进度:0-100
"message": "正在导出第500条",
"download_url": null // 下载链接(成功时返回)
}
轮询规则:
- 间隔:3秒
- 超时:5分钟(超时后停止轮询,提示用户刷新)
- 失败:显示错误信息,停止轮询
三、WebSocket方案
PRD写法
功能:数据导出
异步方案:WebSocket
流程:
1. 用户打开页面,建立WebSocket连接
2. 用户点击"导出"
3. 后端创建导出任务
4. 后端通过WebSocket推送进度
5. 任务完成后,推送下载链接
6. 用户点击下载
WebSocket消息格式:
{
"type": "task_progress", // 消息类型
"task_id": "123",
"status": "processing",
"progress": 50,
"message": "正在导出第500条"
}
{
"type": "task_complete",
"task_id": "123",
"status": "success",
"download_url": "https://xxx.com/file.xlsx"
}
四、回调方案
PRD写法
功能:支付回调
异步方案:回调
流程:
1. 用户发起支付
2. 后端调用支付平台接口
3. 支付平台返回支付链接
4. 用户完成支付
5. 支付平台回调我方接口
6. 我方更新订单状态
7. 前端轮询订单状态(或WebSocket推送)
回调接口:POST /api/payment/callback
请求参数:
{
"order_no": "ORD123",
"trade_no": "PAY456",
"status": "success",
"amount": 199.00,
"sign": "xxx" // 签名
}
响应:
{
"code": 200,
"message": "success"
}
安全要求:
- 验证签名
- 验证订单状态(防止重复回调)
- 记录回调日志
五、异步任务设计实施步骤
步骤1:分析业务场景
在设计异步任务前,需要分析业务场景:
- **识别耗时操作:**哪些操作需要较长时间(>3秒)
- **分析用户需求:**用户是否需要实时看到进度
- **评估实时性要求:**对实时性的要求有多高
- **考虑技术成本:**不同方案的技术实现成本
在梳理异步任务设计方案时,可以使用思维导图工具来整理不同场景和方案的对应关系,比如使用AI思维导图工具,输入业务场景和技术要求,自动生成结构化的异步任务设计思维导图,帮助快速确定最适合的方案。
步骤2:选择异步方案
根据业务场景,选择合适的异步方案:
- **轮询:**适用于简单场景,实时性要求不高
- **WebSocket:**适用于实时性要求高的场景
- **回调:**适用于第三方服务场景
步骤3:设计任务状态
设计任务状态流转:
- **状态定义:**pending(待处理)、processing(处理中)、success(成功)、failed(失败)
- **状态流转:**明确状态之间的转换规则
- **进度信息:**设计进度字段(0-100)和进度消息
步骤4:实现任务管理
实现任务管理功能:
- **任务创建:**创建任务,返回task_id
- **任务查询:**提供查询任务状态的接口
- **任务取消:**支持取消正在执行的任务
- **任务重试:**支持失败任务的重试
六、实际案例详解
案例1:电商数据导出(轮询方案)
**业务场景:**用户需要导出大量订单数据(10万条),导出需要5-10分钟。
**方案选择:**轮询方案(简单场景,实时性要求不高)
实现方案:
1. 前端实现:
- 用户点击"导出"按钮
- 调用创建任务接口:POST /api/export/orders
- 获取task_id
- 每3秒调用查询接口:GET /api/tasks/{task_id}
- 显示进度条和进度信息
- 任务完成后,显示下载链接
2. 后端实现:
- 创建导出任务,返回task_id
- 异步执行导出任务(使用队列)
- 更新任务状态和进度
- 生成Excel文件,上传到OSS
- 更新任务状态为success,返回下载链接
3. 数据库设计:
CREATE TABLE tasks (
id BIGINT PRIMARY KEY,
user_id BIGINT,
type VARCHAR(50), -- export/import/process
status VARCHAR(20), -- pending/processing/success/failed
progress INT DEFAULT 0, -- 0-100
message TEXT,
result_url VARCHAR(500), -- 结果文件URL
error_message TEXT,
created_at DATETIME,
updated_at DATETIME
);
4. 接口设计:
POST /api/export/orders
请求:{ "start_date": "2023-01-01", "end_date": "2023-12-31" }
响应:{ "task_id": "123", "message": "任务已创建" }
GET /api/tasks/{task_id}
响应:{
"status": "processing",
"progress": 50,
"message": "正在导出第50000条",
"download_url": null
}
5. 轮询实现(JavaScript):
const pollTaskStatus = async (taskId) => {
const maxAttempts = 100; // 最多轮询100次(5分钟)
let attempts = 0;
const poll = async () => {
if (attempts >= maxAttempts) {
alert('导出超时,请刷新页面重试');
return;
}
const response = await fetch(`/api/tasks/${taskId}`);
const data = await response.json();
updateProgress(data.progress, data.message);
if (data.status === 'success') {
showDownloadLink(data.download_url);
} else if (data.status === 'failed') {
showError(data.error_message);
} else {
attempts++;
setTimeout(poll, 3000); // 3秒后再次查询
}
};
poll();
};
**效果:**用户点击导出后,可以看到实时进度,任务完成后自动显示下载链接,体验良好。
案例2:实时数据处理(WebSocket方案)
**业务场景:**用户上传文件后,需要实时看到文件处理进度(解析、验证、导入)。
**方案选择:**WebSocket方案(实时性要求高)
实现方案:
1. 前端实现:
- 用户上传文件
- 建立WebSocket连接
- 发送任务创建消息
- 接收进度推送消息
- 更新进度条
- 任务完成后,显示结果
2. 后端实现:
- 接收文件上传
- 创建处理任务
- 建立WebSocket连接(存储连接信息)
- 异步处理文件(使用队列)
- 处理过程中,通过WebSocket推送进度
- 处理完成后,推送结果
3. WebSocket消息格式:
客户端 -> 服务端:
{
"type": "create_task",
"file_id": "123"
}
服务端 -> 客户端:
{
"type": "task_progress",
"task_id": "456",
"status": "processing",
"progress": 50,
"message": "正在解析第1000行"
}
{
"type": "task_complete",
"task_id": "456",
"status": "success",
"result": { "total": 10000, "success": 9800, "failed": 200 }
}
4. 连接管理:
- 使用Redis存储WebSocket连接(key: user_id, value: connection_id)
- 连接断开时,清理连接信息
- 支持自动重连(最多3次)
- 重连失败后,降级为轮询
**效果:**用户可以看到实时的处理进度,体验流畅,无需手动刷新。
案例3:支付回调(回调方案)
**业务场景:**用户支付完成后,支付平台回调我方接口更新订单状态。
**方案选择:**回调方案(第三方服务场景)
实现方案:
1. 支付流程:
- 用户发起支付
- 后端调用支付平台接口,创建支付订单
- 返回支付链接给前端
- 用户完成支付
- 支付平台回调我方接口
- 我方更新订单状态
- 前端轮询订单状态(或WebSocket推送)
2. 回调接口设计:
POST /api/payment/callback
请求参数:
{
"order_no": "ORD202312010001",
"trade_no": "PAY202312010001",
"status": "success", // success/failed
"amount": 199.00,
"pay_time": "2023-12-01 10:00:00",
"sign": "xxx" // 签名
}
响应:
{
"code": 200,
"message": "success"
}
3. 安全验证:
- 验证签名(防止伪造)
- 验证订单状态(防止重复回调)
- 验证金额(防止金额篡改)
- 记录回调日志(便于排查问题)
4. 幂等处理:
- 使用订单号 + 交易号作为唯一标识
- 如果订单已处理,直接返回成功
- 使用数据库事务保证原子性
5. 重试机制:
- 支付平台会重试(通常3次,间隔递增)
- 我方需要支持幂等,避免重复处理
- 如果回调失败,支付平台会继续重试
**效果:**支付完成后,订单状态自动更新,用户无需手动刷新,体验流畅。
七、常见错误及解决方案
错误1:轮询间隔设置不合理
**表现:**轮询间隔太短,浪费服务器资源;间隔太长,用户等待时间长。
**示例:**轮询间隔设置为1秒,导致服务器压力大。
解决方案:
- 根据任务类型设置合理的间隔:简单任务3-5秒,复杂任务5-10秒
- 支持动态调整:任务开始时间隔短,接近完成时间隔长
- 设置最大轮询次数,避免无限轮询
错误2:WebSocket连接管理不当
**表现:**连接断开后没有重连,用户看不到进度;连接泄漏,占用服务器资源。
**示例:**用户刷新页面后,WebSocket连接断开,但没有重连。
解决方案:
- 实现自动重连机制(最多3次)
- 重连失败后,降级为轮询
- 页面关闭时,清理连接
- 使用心跳机制,检测连接状态
错误3:回调接口未做幂等处理
**表现:**支付平台重复回调,导致订单重复处理。
**示例:**支付平台重试3次,订单状态被更新3次。
解决方案:
- 使用订单号 + 交易号作为唯一标识
- 如果订单已处理,直接返回成功
- 使用数据库事务保证原子性
- 记录回调日志,便于排查问题
错误4:任务状态更新不及时
**表现:**任务已完成,但状态没有及时更新,用户看不到结果。
**示例:**导出任务已完成,但前端还在轮询。
解决方案:
- 任务完成后,立即更新状态
- 使用消息队列,确保状态更新可靠
- 定期检查任务状态,处理异常任务
错误5:未处理任务失败情况
**表现:**任务失败后,用户不知道原因,无法重试。
**示例:**导出任务失败,但前端只显示"失败",没有错误信息。
解决方案:
- 记录详细的错误信息
- 前端显示错误信息,提示用户重试
- 支持任务重试功能
- 提供客服联系方式,便于用户反馈
八、最佳实践
实践1:根据场景选择方案
根据业务场景选择合适的异步方案:
- **简单场景:**使用轮询,实现简单,成本低
- **实时性要求高:**使用WebSocket,体验好
- **第三方服务:**使用回调,解耦
- **混合方案:**WebSocket + 轮询降级,保证可靠性
在选择方案时,可以使用思维导图工具来整理不同场景和方案的对应关系,比如使用AI思维导图工具,输入业务场景和技术要求,自动生成结构化的异步任务设计思维导图,帮助快速确定最适合的方案。
实践2:设计完善的任务状态
设计完善的任务状态流转:
- **状态定义:**pending、processing、success、failed、cancelled
- **进度信息:**进度百分比(0-100)、进度消息
- **结果信息:**成功时返回结果URL,失败时返回错误信息
- **时间信息:**创建时间、开始时间、完成时间
实践3:实现可靠的任务管理
实现可靠的任务管理功能:
- **任务持久化:**任务信息存储在数据库中
- **任务队列:**使用消息队列处理任务,保证可靠性
- **任务监控:**监控任务执行情况,及时发现问题
- **任务清理:**定期清理过期任务,释放存储空间
实践4:优化用户体验
优化用户体验,提升满意度:
- **进度显示:**显示进度条和进度信息,让用户知道进度
- **错误提示:**任务失败时,显示详细的错误信息
- **重试功能:**支持任务重试,减少用户操作
- **超时处理:**设置超时时间,避免用户无限等待
实践5:保证系统稳定性
保证系统稳定性,避免影响用户体验:
- **限流保护:**限制轮询频率,避免服务器压力过大
- **连接管理:**合理管理WebSocket连接,避免连接泄漏
- **异常处理:**完善的异常处理机制,保证系统稳定
- **监控告警:**监控任务执行情况,及时发现问题
九、FAQ
Q1:轮询间隔多久合适?
建议:3-5秒。太短浪费资源,太长用户等待时间长。可以根据任务类型动态调整:简单任务3秒,复杂任务5秒。
Q2:WebSocket断开怎么办?
建议:自动重连(最多3次),重连失败后降级为轮询。使用心跳机制检测连接状态,及时发现问题。
Q3:回调失败怎么办?
建议:支付平台会重试(通常3次),我方需要做幂等处理。如果回调失败,可以主动查询订单状态,确保数据一致性。
Q4:如何快速设计异步任务方案?
建议:先分析业务场景,确定实时性要求,然后选择合适的方案。可以使用思维导图工具来整理不同场景和方案的对应关系,比如使用AI思维导图工具,输入业务场景和技术要求,自动生成结构化的异步任务设计思维导图,帮助快速确定最适合的方案。
Q5:任务状态如何设计?
建议:定义pending、processing、success、failed、cancelled等状态。记录进度信息(0-100)、进度消息、结果信息、时间信息等。
Q6:如何保证任务可靠性?
建议:使用消息队列处理任务,保证可靠性。任务信息存储在数据库中,支持任务重试。定期检查任务状态,处理异常任务。
Q7:如何优化用户体验?
建议:显示进度条和进度信息,让用户知道进度。任务失败时,显示详细的错误信息。支持任务重试,减少用户操作。设置超时时间,避免用户无限等待。