4. 异步导出任务模型:任务队列、生产者-消费者模式、任务状态机设计
在缓存失效导致数据源压力陡增的场景下,异步导出任务必须从“即时执行”转变为“排队执行”。本章围绕任务队列、生产者-消费者模式与任务状态机三个核心要素,构建一个可降级、可补偿、可追踪的异步导出任务模型。
4.1 任务队列:异步导出的缓冲核心
任务队列是异步导出系统的数据缓冲层,负责接收生产者提交的导出请求,并按顺序分发给消费者处理。在 Redis 缓存失效的降级场景中,任务队列需要具备以下能力:
- 持久化:队列中的任务不因 Redis 宕机而丢失,需支持后端存储(如 MySQL、Kafka)作为兜底。
- 优先级:支持按用户等级或任务紧急程度设置优先级,高优任务可插队。
- 去重:同一用户对同一数据集的重复导出请求,在队列层面合并为一次。
- 限流:控制队列积压深度,防止消费者被突发流量冲垮。
| 队列特性 | 正常模式 | 降级模式(缓存失效) |
|---|---|---|
| 存储介质 | Redis List / Stream | MySQL 任务表 + Redis 兜底 |
| 优先级 | FIFO | 加权优先级队列 |
| 去重窗口 | 5 分钟 | 30 分钟(延长去重窗口) |
| 最大积压 | 10,000 | 1,000(主动拒绝新任务) |
设计要点:降级模式下,队列的“最大积压”应硬性收缩,避免消费者因数据库连接池耗尽而雪崩。同时,去重窗口延长可减少重复数据查询对后端的冲击。
4.2 生产者-消费者模式:解耦与流量削峰
生产者-消费者模式将导出任务的提交与执行完全解耦。生产者只负责将任务描述(元数据)写入队列,消费者从队列中拉取任务并执行实际的导出逻辑。这种模式天然支持流量削峰:即使瞬间涌入大量导出请求,队列也能平滑消化。
关键实践:消费者 Worker 的数量应动态可调。在缓存失效的降级期,减少 Worker 数量(例如从 10 个降为 3 个),降低对数据库和文件系统的并发压力;恢复后逐步增加。
4.3 任务状态机:全生命周期可追踪
每个导出任务从创建到完成(或失败),经历一系列确定的状态转换。状态机设计是任务可观测、可补偿的基础。以下为异步导出任务的标准状态机:
4.4 状态转换与补偿逻辑
状态机中的每个转换都对应明确的业务动作。在缓存失效的降级场景下,以下转换路径需要特别关注:
| 当前状态 | 触发事件 | 目标状态 | 补偿动作 |
|---|---|---|---|
| PENDING | 任务入队成功 | QUEUED | 无 |
| QUEUED | 消费者拉取 | PROCESSING | 设置超时 TTL(默认 5 分钟) |
| PROCESSING | 导出异常 | FAILED | 记录错误原因,释放 Worker |
| FAILED | 重试次数 < 3 | RETRYING | 指数退避等待后重新入队 |
| RETRYING | 重新入队成功 | QUEUED | 更新重试计数 |
| PROCESSING | 超时未完成 | CANCELLED | 回滚临时文件,释放锁 |
| QUEUED | 用户主动取消 | CANCELLED | 从队列中移除任务 |
降级补偿要点:当 Redis 缓存失效导致数据库查询变慢时,PROCESSING 状态的任务更容易超时。此时应缩短超时 TTL(例如从 5 分钟降为 2 分钟),并增加 FAILED→RETRYING 的退避间隔(从 10 秒增至 60 秒),避免重试风暴。
4.5 任务元数据设计
每个任务在队列中携带的元数据决定了消费者如何执行导出。以下为推荐的任务元数据结构:
{
"taskId": "exp_20250321_001",
"userId": "u_12345",
"type": "order_export",
"params": {
"startDate": "2025-03-01",
"endDate": "2025-03-21",
"format": "xlsx"
},
"priority": 5,
"status": "QUEUED",
"retryCount": 0,
"maxRetries": 3,
"ttl": 300,
"createTime": 1711000000,
"source": "web"
}
- taskId:全局唯一,用于去重和追踪。
- type:导出类型,消费者根据 type 路由到不同的导出处理器。
- params:导出参数,包含查询条件、文件格式等。
- priority:优先级数值,数值越小优先级越高(1 最高,10 最低)。
- retryCount / maxRetries:控制重试次数,防止死循环。
- ttl:任务超时时间(秒),超时后自动标记为 CANCELLED。
- source:任务来源,用于区分正常请求与降级补偿产生的任务。
降级标识:在降级模式下,生产者应在 source 字段中标记为 "degraded",消费者据此调整行为——例如跳过缓存直接查数据库,或使用更保守的查询分页策略。
4.6 任务队列的 Redis 实现示例
以下代码展示了使用 Redis Stream 实现任务队列的生产者与消费者核心逻辑:
// 生产者:提交导出任务
public function submitExportTask(array $taskData): string
{
$taskId = $this->generateTaskId();
$taskData['taskId'] = $taskId;
$taskData['status'] = 'PENDING';
$taskData['createTime'] = time();
// 写入 Redis Stream
$this->redis->xAdd('export:task:stream', '*', $taskData);
// 同时写入 MySQL 任务表做持久化兜底
$this->mysql->insert('export_tasks', $taskData);
return $taskId;
}
// 消费者:拉取并处理任务
public function consumeExportTask(): void
{
// 从 Stream 中读取未确认的任务
$messages = $this->redis->xReadGroup(
'export:consumer:group',
'worker_1',
['export:task:stream' => '>'],
1, // 每次只取 1 个任务
0 // 阻塞等待
);
foreach ($messages as $messageId => $taskData) {
try {
// 更新状态为 PROCESSING
$this->updateTaskStatus($taskData['taskId'], 'PROCESSING');
// 执行导出逻辑
$this->executeExport($taskData);
// 确认消息已处理
$this->redis->xAck('export:task:stream', 'export:consumer:group', [$messageId]);
$this->updateTaskStatus($taskData['taskId'], 'COMPLETED');
} catch (\Exception $e) {
// 失败处理:重试或标记失败
$this->handleFailure($taskData, $e);
}
}
}
生产建议:Redis Stream 的消费者组机制天然支持消息确认(ACK)和重新投递,非常适合异步导出任务的可靠性要求。配合 MySQL 任务表做全量持久化,即使 Redis 宕机,也能从 MySQL 恢复未完成的任务。