函数计算异步任务在高并发AI Agent场景下的幂等性与去重实践(真实踩坑+可落地代码)
大家好,我是Lee,坐标某大厂,日常主要做AI应用后端,这两年几乎所有在线服务都跑在阿里云函数计算(FC)上。
前段时间帮一个ToC智能助手产品扛高峰(日活200w+,早8点和晚6点各一个尖峰),结果被异步任务重复执行坑惨了:
- 用户收到N条完全一样的回复
- 数据库重复写入相同记录
- LLM token和FC账单莫名多出几千块……
排查下来,主要三类原因:
- 用户/前端狂点(防抖失效或脚本刷)
- 上游消息队列/EventBridge等偶发重投递
- FC异步调用本身的At-Least-Once + 重试机制
今天把我们最终跑通的四层去重组合拳完整开源出来,全部可直接抄,附带真实数据和血的教训。
核心思路:业务层 + 平台层双保险,而不是赌单一层不出问题。
一、FC异步为什么容易重复?
官方文档写得很明白:FC的异步调用(HTTP异步、事件源触发、定时器、MNS/Kafka/EventBridge等)都是 At-Least-Once 语义。
在AI Agent场景尤其狠:
- 一个用户意图 → 多轮工具调用(tool call → LLM → tool call)
- 链路越长,重试窗口越大
- 重复概率指数级上升
二、四层防重方案(从外到内,层层递进)
层1:全链路透传 requestId(最外层,性价比最高)
用户请求进来第一步生成全局唯一 requestId(雪花ID + user_id后8位混淆),全程Header透传:
- HTTP Header:
X-Request-ID: xxx - 所有工具调用、回调、异步payload都必须带上
- 前端收到响应先比对requestId,已处理过的直接丢弃
哪怕FC重复触发,前端也能秒丢。
层2:Redis + Lua 分布式轻量锁(拦截率最高的一层)
去重粒度:用户 + 意图 + 业务唯一标识
key 设计示例:
fc:dedup:{user_id}:{intent_type}:{biz_key}
# 例子:fc:dedup:123456789:reply_message:session_abc123_order456
Lua脚本(set-if-absent + 相同request允许幂等):
-- dedup.lua
local key = KEYS[1]
local req_id = ARGV[1]
local ttl = tonumber(ARGV[2]) -- 建议180~300
local exists = redis.call('GET', key)
if exists then
if exists == req_id then
return 'REPEAT_SAME' -- 同一个req,幂等放行
else
return 'REPEAT_OTHER' -- 别的req先占位,丢弃本次
end
else
redis.call('SET', key, req_id, 'EX', ttl)
return 'FIRST'
end
Python 调用(redis-py):
import redis
r = redis.Redis(...) # 你的连接配置
DEDUPE_SCRIPT = """
-- 把上面的Lua完整粘贴在这里
"""
def try_dedup(user_id: str, intent: str, biz_key: str, request_id: str, ttl: int = 180) -> str:
key = f"fc:dedup:{user_id}:{intent}:{biz_key}"
result = r.eval(DEDUPE_SCRIPT, 1, key, request_id, ttl)
return result.decode() if isinstance(result, bytes) else result
FIRST→ 正常执行REPEAT_SAME→ 幂等放行(适合更新/通知类操作)REPEAT_OTHER→ 直接return + 打warn日志
这一层单独跑就能挡掉 85%+ 的重复,延迟增加 < 2ms。
层3:FC Handler 内二次指纹校验
利用event自带eventId(部分源支持)+ payload稳定指纹:
import hashlib
import json
def handler(event, context):
evt = json.loads(event)
payload = evt.get("payload", {
})
req_id = payload.get("requestId")
if req_id:
# 已在上层Redis处理,这里可信任或再check
pass
else:
# fallback指纹
sorted_json = json.dumps(payload, sort_keys=True, separators=(',',':'))
fp = hashlib.md5(sorted_json.encode()).hexdigest()
short_key = f"fc:fp:{fp[:16]}" # 短ttl 30s 二次防
# 再走一次Redis check(类似try_dedup逻辑)
...
层4:数据库最终兜底
所有写操作表必加:
request_idvarchar(64)- 唯一索引
uk_request_id或复合唯一索引
Redis彻底挂掉还有数据库保底。
三、真实效果数据(2025年底~2026年初,部分脱敏)
- 峰值QPS ≈ 4200
- 无去重重复率 ≈ 11.7%
- 只加requestId透传 ≈ 3.1%
- 加Redis Lua层 ≈ 0.42%
- 四层全开 ≈ 0.08% 以内(基本可忽略)
收益:
- LLM无效调用减少 ≈ 22%
- FC账单降低 ≈ 18%
- DB写压力降低 ≈ 15%
四、几条必须记住的坑
- 永远不要信“上游已经防重了”——上游防的是它自己
- TTL要覆盖业务最长链路,但别超过5分钟(内存压力)
- 日志强制打印 requestId + fingerprint,排查神器
- 灰度第一件事:挂重复率大盘
公式:(Redis拒绝数 + DB唯一冲突数) / 总调用量 - 同一个request重复进来时要允许幂等通过,别一刀切拒绝
有朋友也在用FC跑Agent、多轮对话、异步工作流吗?你们是怎么防重的?有没有遇到更离谱的重复场景?
也很好奇阿里云FC未来会不会原生支持request级去重(配置一下就完事那种,真的很香)。