在分布式系统和前后端分离架构中,异步任务处理是每个开发者必须掌握的核心技能。想象这样一个场景:用户提交一个视频转码请求,如果让客户端傻等30秒直到转码完成,这种同步阻塞的体验简直是一场灾难。这就是为什么我们需要异步任务设计——将耗时操作放到后台执行,通过特定机制通知客户端结果。
目前主流的异步通知机制有三种技术路线:
我曾在一个电商促销系统中同时实现过这三种方案。当秒杀活动开始时,订单状态更新采用WebSocket保证实时性;物流跟踪使用轮询降低服务器压力;支付结果则通过回调通知商户系统。这种混合方案比单一机制更能适应复杂业务场景。
最简单的轮询就是setInterval定时请求API。但实际项目中需要考虑更多细节:
javascript复制// 前端实现示例
async function pollOrderStatus(taskId) {
const interval = setInterval(async () => {
const res = await fetch(`/api/tasks/${taskId}`);
if (res.status === 'completed') {
clearInterval(interval);
// 处理完成逻辑
}
}, 3000); // 3秒间隔
}
关键经验:轮询间隔需要根据业务特点调整。金融类业务可能需要1秒间隔,而报表生成可以设为30秒。太频繁会增加服务器压力,太慢会影响用户体验。
在实际项目中,我推荐使用指数退避算法(Exponential Backoff)优化轮询:
这种方案在我负责的物流跟踪系统中,使API调用量减少了78%。
服务端需要专门设计任务状态查询接口:
java复制@GetMapping("/tasks/{id}")
public TaskResult getTaskStatus(@PathVariable String id) {
Task task = taskService.getTask(id);
return new TaskResult(
task.getId(),
task.getStatus(), // RUNNING/SUCCESS/FAILED
task.getProgress(), // 0-100
task.getResultData()
);
}
注意要添加Redis缓存层,避免频繁查询数据库。我曾遇到一个未加缓存的轮询接口,在促销期间直接打垮了MySQL。
WebSocket特别适合需要实时反馈的场景。这是Spring Boot的实现示例:
java复制@ServerEndpoint("/task-updates/{userId}")
public class TaskUpdateEndpoint {
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
// 将session与用户ID关联
}
@OnMessage
public void onMessage(String message) {
// 处理客户端消息
}
public static void sendUpdate(String userId, TaskUpdate update) {
// 向特定用户发送任务更新
}
}
前端需要处理连接状态:
javascript复制const socket = new WebSocket(`wss://api.example.com/task-updates/${userId}`);
socket.onmessage = (event) => {
const update = JSON.parse(event.data);
// 更新UI
};
在实际部署WebSocket时,必须考虑:
在我们的IM系统中,未实现心跳机制导致30%的连接在NAT超时后丢失。添加心跳后稳定性提升到99.9%。
回调需要明确定义:
这是我们的回调请求示例:
http复制POST /callback/order-update HTTP/1.1
Content-Type: application/json
X-Signature: sha256=5d41402abc4b2a76b9719d911017c592
{
"task_id": "T123456",
"status": "completed",
"result": {
"video_url": "https://example.com/videos/123.mp4"
},
"timestamp": 1630000000
}
回调接口必须包含:
曾有一次未经验证的回调接口被恶意调用,导致系统创建了数百个虚假订单。现在所有回调接口都必须通过签名验证。
完整的异步任务PRD应包含:
markdown复制## 视频转码任务系统
### 任务创建
- 触发条件:用户上传新视频文件
- 初始状态:PENDING
- 超时时间:30分钟
### 状态通知
| 状态变化 | 通知方式 | 频率/条件 |
|----------------|------------------------|-------------------|
| PENDING→RUNNING | WebSocket实时推送 | 状态变更立即触发 |
| RUNNING→SUCCESS | 回调+WebSocket | 任务完成时 |
| RUNNING→FAILED | 轮询API状态变更 | 每10秒检查一次 |
### 结果数据
```json
{
"success": {
"video_url": "string",
"duration": "number",
"thumbnail": "string"
},
"error": {
"code": "INVALID_FORMAT",
"message": "不支持的视频格式"
}
}
在最近的大文件导出系统中,我设计了这样的混合方案:
这个方案既保证了初期的高实时性,又避免了长期保持WebSocket连接的开销。实测显示:
实现关键代码:
python复制def handle_export_task(task_id):
# 初始阶段使用WebSocket
websocket.send_initial_progress(task_id)
# 30秒后切换为轮询
time.sleep(30)
websocket.send_mode_switch(task_id)
# 后台继续处理任务
result = process_export(task_id)
# 完成后双通道通知
callback_client.notify(task_id, result)
websocket.send_completion(task_id, result)
在设计异步任务系统时,需要监控这些核心指标:
我们的监控面板包含这些关键图表:
使用Prometheus+Granfa实现的监控示例:
yaml复制# Prometheus监控规则
- alert: HighTaskQueueTime
expr: avg(task_queue_time_seconds) by (service) > 30
for: 5m
labels:
severity: warning
annotations:
summary: "High task queue time in {{ $labels.service }}"
根据我们的故障统计,主要问题包括:
我们建立的防御措施:
补偿任务的核心逻辑:
go复制func runCompensationJob() {
for {
// 查找超时任务
tasks := db.FindTimeoutTasks(time.Now().Add(-30*time.Minute))
for _, task := range tasks {
// 重新触发通知
notifyClient(task)
// 更新为超时状态
db.UpdateTaskStatus(task.ID, "TIMEOUT")
}
time.Sleep(5 * time.Minute)
}
}
在实际项目中,这套机制帮助我们找回了超过15%的"丢失"任务,大大提高了系统可靠性。