## 1. 项目背景与问题定位 去年在开发一个AIGC内容生成平台时,我们遇到了一个诡异的线上问题:用户上传的图片/文本有时会莫名其妙消失,后台日志显示"任务已完成",但客户端始终收不到结果。更糟的是,当用户快速连续提交多个任务时,整个队列会陷入死锁状态,需要手动重启服务。经过长达两周的排查,最终定位到这是典型的异步竞态问题——任务秒传机制与状态机更新之间的时序冲突。 这个问题在AIGC领域尤为突出,因为: 1. 生成任务通常需要5-30秒不等 2. 用户习惯连续提交相似内容(比如批量生成电商文案) 3. 为提升体验普遍采用"秒传"优化(当哈希值相同时直接返回已有结果) ## 2. 竞态条件深度解析 ### 2.1 秒传机制的陷阱 我们最初的秒传实现逻辑是这样的: ```python async def handle_upload(file): file_hash = calculate_hash(file) if cache.exists(file_hash): return {"status": "done", "result": cache.get(file_hash)} task_id = generate_task_id() await redis.set(f"task:{task_id}", "processing") await queue.push(task_id, file) return {"status": "processing", "task_id": task_id}问题出在缓存检查(cache.exists)和任务创建(queue.push)这两个操作不是原子性的。在高并发场景下可能出现:
- 请求A检查缓存:不存在
- 请求B检查缓存:不存在
- 请求A创建任务
- 请求B创建任务 → 重复任务诞生!
2.2 状态死锁的成因
当两个相同任务同时进入处理队列时:
- Worker1获取任务A开始处理
- Worker2获取任务A副本也开始处理
- 先完成的Worker更新状态为"done"
- 后完成的Worker再次更新状态 → 可能覆盖有效结果
- 客户端根据task_id轮询时,可能拿到中间状态
3. 解决方案设计与实现
3.1 分布式锁改造
最终采用的方案组合:
from redlock import RedLock async def handle_upload(file): file_hash = calculate_hash(file) lock_key = f"lock:{file_hash}" async with RedLock(lock_key, ttl=3000): if cache.exists(file_hash): return {"status": "done", "result": cache.get(file_hash)} task_id = generate_task_id() await redis.setex(f"task:{task_id}", 3600, "processing") await queue.push(task_id, file, dedup_key=file_hash) return {"status": "processing", "task_id": task_id}关键改进点:
- 使用RedLock实现文件哈希级别的互斥
- 任务队列支持dedup_key自动去重
- Redis状态设置过期时间避免堆积
3.2 状态机强化
新增状态校验中间件:
async def update_task_status(task_id, status): current = await redis.get(f"task:{task_id}") if current == "done": raise InvalidStateError("Cannot update finished task") # 状态流转校验 valid_transitions = { "processing": ["done", "failed"], "failed": ["processing"] # 允许重试 } if status not in valid_transitions.get(current, []): raise InvalidStateError(f"Illegal transition: {current}->{status}") await redis.set(f"task:{task_id}", status)4. 性能优化与压测结果
4.1 锁粒度优化
最初的全局锁方案导致吞吐量下降40%,通过分级锁解决:
- 第一层:内存锁(asyncio.Lock)过滤90%请求
- 第二层:分布式锁(RedLock)处理冲突
_lock = asyncio.Lock() async def handle_upload(file): file_hash = calculate_hash(file) async with _lock: if file_hash in _quick_cache: return _quick_cache[file_hash] # 继续分布式锁流程...4.2 基准测试对比
模拟100并发用户连续提交相同文件:
| 方案 | 成功率 | 平均耗时 | 系统负载 |
|---|---|---|---|
| 原始方案 | 68% | 2.4s | 高 |
| 纯分布式锁 | 100% | 1.8s | 中 |
| 分级锁优化 | 100% | 1.2s | 低 |
5. 实战经验总结
5.1 必坑指南
Redis过期时间陷阱:我们曾因未设置TTL导致内存溢出,建议:
# 错误示范 await redis.set("key", "value") # 正确做法 await redis.setex("key", 3600, "value")锁的可见性问题:某次上线后锁失效,发现是Redis主从同步延迟导致,解决方案:
- 使用RedLock等多节点确认机制
- 对关键操作强制读主库
5.2 监控建议
在Prometheus中添加这些关键指标:
metrics: - name: task_race_condition_count help: "检测到的竞态条件次数" labels: ["type"] - name: task_state_inconsistency help: "状态不一致事件" labels: ["from_state", "to_state"]这个优化方案上线后,我们的AIGC平台任务错误率从3.2%降至0.02%,特别是在处理批量生成场景时,系统稳定性得到显著提升。现在即使用户连续提交100个相同文案生成请求,也能确保只有一个实际任务进入处理队列。