数据说明 本文以果冻试玩移动任务平台的研发场景为背景。字段、数量、延迟和成功率均为脱敏演示或本地实验数据,用于说明设计方法,不代表线上实时指标;本文不构成收益承诺、投资建议或软件下载推广。
摘要
移动任务审核往往依赖截图、设备信息和渠道回传,不适合在用户提交接口中同步等待。本文以果冻试玩任务审核场景为例,将接收、校验、外部验证、决策、结算和通知拆成异步阶段,重点讨论消息重复、消费者崩溃、下游超时、死信队列和人工补偿。
1. 同步审核为什么容易失控
同步接口把用户连接、第三方渠道和内部结算绑在一条链路上。任一环节超过网关时限,客户端就会误以为失败并重复提交;而服务端可能已完成部分步骤,最终形成"用户看到失败,后台实际成功"的不确定状态。
- 提交接口只做轻量校验和落库,尽快返回 submission_id 与预计处理状态。
- 每个异步阶段都有独立状态、重试次数和最后错误码。
- 消息至少投递一次,因此消费者必须按业务键幂等,而不是假设消息只来一次。
- 超过自动重试阈值后进入死信或人工队列,不能无限重试挤占正常流量。
2. 系统组件与职责
表1 异步审核组件职责
|----------------------|-----------------------|--------------------|
| 组件 | 职责 | 边界 |
| Submission API | 接收材料并生成 submission_id | 不等待外部审核 |
| Outbox Relay | 把本地事件投递到消息队列 | 按 event_id 去重 |
| Precheck Worker | 格式、时效、重复提交校验 | 失败可直接给出明确原因 |
| Channel Verifier | 查询或等待渠道数据 | 处理超时与回传延迟 |
| Review Engine | 组合规则得出审核结论 | 规则版本可追溯 |
| Settlement Worker | 生成积分结算单 | 按 submission_id 幂等 |
| Notification Worker | 更新用户可见状态 | 通知失败不回滚结算 |
| Compensation Console | 人工复核、重放和纠错 | 操作全量审计 |
口径:组件可按团队规模合并部署,但职责和幂等边界应保持清晰。
3. 消息结构:让事件可以安全重放
表2 审核事件的最小字段集
|----------------|--------------------------------------------------|
| 字段 | 说明 |
| event_id | 全局唯一事件标识 |
| event_type | SUBMISSION_CREATED / VERIFIED / REVIEW_DECIDED 等 |
| aggregate_id | submission_id,聚合根标识 |
| biz_key | 用户 + 任务 + 任务周期形成的稳定业务键 |
| occurred_at | 业务事件发生时间 |
| trace_id | 跨服务链路追踪标识 |
| schema_version | 消息结构版本,便于兼容升级 |
| payload | 必要字段;避免把完整敏感材料放入消息 |
| retry_count | 当前重试次数,由消费框架或业务层维护 |
安全建议:截图等大文件只在受控对象存储中保存,消息内传短时访问引用,不传公开 URL。
consume(message):
if processed_event.exists(message.event_id):
return ACK
begin transaction
submission = load_for_update(message.aggregate_id)
result = handle(submission, message)
save_stage_result(result)
insert processed_event(message.event_id)
insert outbox(next_event(result))
commit
return ACK
processed_event 解决消息重复,阶段结果解决业务重复,Outbox 解决数据库提交成功但下一条消息丢失。三者关注的问题不同,不能只保留其中一个。
4. 超时、重试与死信策略
表3 指数退避重试示例
|--------|----------|------------|-----------|
| 阶段 | 等待时间 | 适用原因 | 动作 |
| 第1次 | 1 分钟 | 短暂网络抖动 | 自动重试 |
| 第2次 | 5 分钟 | 下游短时不可用 | 自动重试 |
| 第3次 | 15 分钟 | 渠道回传延迟 | 自动重试并告警 |
| 第4次 | 60 分钟 | 持续性异常 | 降级查询 / 限流 |
| 超过阈值 | 不再自动 | 不可恢复或需业务判断 | 进入死信与人工复核 |
口径:等待时间为演示配置;实际值应根据渠道 SLA、任务时效和队列容量调整,并加入随机抖动。
表4 错误分类决定是否重试
|----------------------|---------|---------|------------|
| 错误码 | 含义 | 可重试 | 处理方式 |
| FORMAT_INVALID | 材料格式错误 | 否 | 直接失败并提示重传 |
| CHANNEL_TIMEOUT | 渠道查询超时 | 是 | 按退避策略重试 |
| CHANNEL_NOT_FOUND | 暂未查到回传 | 有限重试 | 到期转人工或失败 |
| RULE_VERSION_MISSING | 规则版本缺失 | 否 | 告警并阻断批次 |
| SETTLE_CONFLICT | 结算幂等冲突 | 条件性 | 读取首次结算结果 |
| STORAGE_UNAVAILABLE | 材料存储不可用 | 是 | 短时重试,超阈值降级 |
原则:明确的业务失败不重试;只有可能自行恢复的技术异常才进入自动重试。
5. 失败补偿:不是把所有步骤倒放一次
异步流程通常无法使用跨服务大事务。补偿应针对已发生的业务事实:审核已通过但结算失败,只需重试结算;结算已成功但通知失败,只需补发通知;错误结算则新增反向账务流水,而不是删除原记录。
- 记录阶段完成标记和结果版本,补偿前先确定最后一个成功阶段。
- 生成 compensation_order_id,所有补偿动作按该标识幂等。
- 自动补偿只处理确定性动作;需要解释规则或判断证据时转人工。
- 人工重放前展示原消息、错误码、已执行步骤和潜在影响,操作写审计日志。
6. 故障注入验证
表5 故障注入演示结果
|-----------------------|-------------|--------------------|--------|--------|
| 注入故障 | 主要风险 | 恢复机制 | 结果 | 结论 |
| 重复投递 2 次 | 重复结算 | event_id + 业务单唯一键 | 0 笔重复 | 通过 |
| Worker 提交后崩溃 | 消息再次投递 | processed_event 去重 | 状态不回退 | 通过 |
| 渠道超时 30 秒 | 审核积压 | 超时 + 退避 + 隔离 | 恢复后清空 | 通过 |
| Outbox Relay 停止 10 分钟 | 事件未投递 | 扫描未发送事件 | 重启后补投 | 通过 |
| 结算服务返回 500 | APPROVED 滞留 | 独立结算重试 | 无丢单 | 通过 |
| 规则版本不存在 | 错误判定 | 批次熔断 | 无错误结论 | 通过 |
数据口径:实验环境演示结果,用于验证恢复路径,不代表生产可用性承诺。
7. 容量压测与背压
表6 合成消息负载下的演示压测
|---------------|-------------|-----------|----------|---------|
| 输入速率(条/秒) | P95处理延迟 | 处理错误率 | 队列积压 | 判断 |
| 100 | 90 ms | 0.00% | 0 | 稳定 |
| 300 | 140 ms | 0.02% | 120 | 稳定 |
| 500 | 260 ms | 0.08% | 1,850 | 可接受,需观察 |
| 800 | 780 ms | 0.45% | 18,600 | 触发限流 |
| 1,000 | 1,430 ms | 1.20% | 42,300 | 超过演示容量 |
数据口径:单一实验拓扑的合成负载;硬件、消息大小和下游延迟变化都会改变结果。
从演示数据看,系统在 500 条/秒附近仍可工作,但 800 条/秒开始出现明显积压。此时应该通过入口限流、按任务类型隔离队列和消费者水平扩容形成背压,而不是无上限地增加重试。
8. 可观测性指标
- 按阶段统计吞吐、成功率、P50/P95/P99 延迟和错误码分布。
- 同时观察队列积压条数与最老消息年龄;后者更能反映用户等待时长。
- 监控重试次数、死信数量、人工复核滞留和补偿成功率。
- trace_id 贯穿提交、审核、结算和通知,客服可由 submission_id 反查完整链路。
结语
一个可信的异步审核系统不以"消息不重复、下游不超时"为前提,而是承认这些异常一定会发生,并让每个阶段都能识别重复、限定重试、保留结果、主动补偿。对于果冻试玩,公开这类边界和验证方法,比泛泛描述"审核快、到账快"更能建立技术可信度。