情况:
AI模拟面试的RAG部分中,将生成的报告回传到java端时如若产生网络错误产生的报告直接丢失,或者重复发送。
为此利用如下机制:
Tenacity (Python 第三方重试框架)
-
角色:系统的"减震器"。
-
技术细节 :它是一个通过 Python 装饰器(Decorator)实现的高级重试库。你使用了它的
wait_exponential(指数退避算法),这比简单的time.sleep()强大得多,它是工业界解决"拥堵重试"的标准算法。
HTTP Headers (网络协议层自定义标识)
-
角色:系统的"防伪标签"。
-
技术细节 :你利用了 HTTP 协议的扩展性,在 Request Header 中自定义了
X-Idempotency-Key字段来携带session_id。这是一种非侵入式的数据传递,不会破坏原有 JSON Payload 的结构。
Redis List (内存数据库数据结构)
-
角色:系统的"冷冻保险箱"。
-
技术细节 :你利用了 Redis 的双端队列特性。通过
lpush(左侧推入)命令,将 Python 字典序列化为 JSON 字符串后安全落盘。Redis 的极速响应保证了兜底写入时不会拖垮主线程。
解决问题的方法论
🛡️ 第一步:用"容错重试"解决偶发波动
-
问题场景:发送请求的那一秒,公司的路由器抖动了,或者 Java 后端正好在处理垃圾回收(GC)卡顿了 1 秒。如果直接报错,这份报告就凭空消失了。
-
解决逻辑 :你封装了
_do_push_with_retry,允许代码在遇到 HTTP 异常时"再试几次"。而且为了防止把 Java 服务器打垮,你要求每次重试的等待时间按指数级拉长(2秒、4秒、8秒),这种"克制的重试"能消化掉 90% 的偶发网络故障。
🛡️ 第二步:用"幂等设计"解决重复执行
-
问题场景 :重试机制是一把双刃剑。如果 Java 其实已经成功收到了报告存进数据库了,仅仅是在给 Python 回复
200 OK时网络断了。此时 Python 误以为没发成功,触发重试再次发送。这就导致 Java 数据库里存了两条一样的面试成绩! -
解决逻辑 :你引入了**幂等性(Idempotency)**思维。在发送时贴上唯一的单号(
session_id)。这个架构设计把防重的压力合法地转移给了 Java 端------Java 端在存库前必须先校验这个单号,如果处理过就直接无视并返回成功。彻底绝杀了"数据重复"的 P0 级 Bug。
🛡️ 第三步:用"死信队列 (DLQ)"解决彻底宕机
-
问题场景:如果 Java 后台正在进行长达半小时的版本升级重启,无论你重试多少次都会失败。程序最终抛出异常,此时如果依然打印一句错误就结束,数据还是会丢失。
-
解决逻辑 :你秉持了**"最终一致性(Eventual Consistency)"**的底层逻辑。当重试弹药全部打光后,你在
except块中捕获了最终异常,将原本要丢弃的数据打包成"死信(Dead Letter)",静悄悄地塞进 Redis 的failed_reports_queue里。数据没有消失,只是转移了阵地,等待未来系统恢复后进行手动或定时的补偿发送。python# ================= 1. 失败落库的兜底逻辑 ================= async def save_failed_report_to_redis(user_id: str, session_id: str, payload: dict): """当所有重试都失败后,把报告塞进 Redis 死信队列,等待后续手动/定时补偿""" try: dead_letter = { "session_id": session_id, "user_id": user_id, "payload": payload, "timestamp": time.time() } # lpush: 从左边推进一个名为 failed_reports_queue 的列表 await redis_client.lpush("failed_reports_queue", json.dumps(dead_letter, ensure_ascii=False)) print(f"🆘 [极致兜底] 会话 {session_id} 的报告已安全存入 Redis 死信队列!数据未丢失。") except Exception as e: print(f"💥 [灾难] 死信队列写入失败,报告彻底丢失: {e}") # ================= 2. 带自动重试机制的发送逻辑 ================= # @retry 魔法:如果遇到网络报错,自动重试。 # wait_exponential(multiplier=1, min=2, max=10):指数退避等待,第一次等2秒,第二次等4秒,第三次等8秒 # stop_after_attempt(3):最多疯狂重试 3 次,不行就放弃抛出异常 @retry(wait=wait_exponential(multiplier=1, min=2, max=10), stop=stop_after_attempt(3)) async def _do_push_with_retry(payload: dict): """执行实际的 HTTP 请求,失败会自动重试""" async with httpx.AsyncClient() as client: response = await client.post( settings.JAVA_REPORT_URL, # ⚠️ 统一配置项名称:确保 .env 里叫这个名字 json=payload, headers={ "X-Idempotency-Key": payload["sessionId"] # 🌟 极其关键的幂等键,防止 Java 端重复存库! }, timeout=15.0 ) # 如果 Java 端返回 500/502/503 等非 2xx 的错误码,主动抛出异常,强制触发 Tenacity 进行重试 response.raise_for_status() return response # ================= 3. 对外暴露的业务调度函数 ================= async def push_report_to_java(user_id: str, session_id: str, report_data: dict): """ 将生成的报告推送给 Java 业务端(已接入重试与死信队列保障) """ # 1. 组装发给 Java 的终极 Payload (这部分你写得很完美) payload = { "userId": user_id, "sessionId": session_id, "score": report_data.get("comprehensiveScore", 0), "reportDetail": json.dumps(report_data, ensure_ascii=False), "contentShortcomings": report_data.get("contentAnalysis", {}).get("shortcomings", ""), "expressionShortcomings": report_data.get("expressionAnalysis", {}).get("shortcomings", "") } try: print(f"🚀 正在将 {session_id} 的报告发送至 Java 业务中心...") # 🌟 核心修改点:丢弃旧的直接发送逻辑,调用我们刚刚写好的带装甲的发送函数! # 如果这里成功了,说明要么是一次成功,要么是经过几次重试后成功了 await _do_push_with_retry(payload) print(f"✅ [跨服协同] 会话 {session_id} 报告推送成功!") except Exception as e: # 🌟 核心兜底点:如果代码走到这里,说明 Tenacity 已经拼命重试了 3 次,但 Java 服务器依然处于瘫痪状态 print(f"❌ [网络绝望] 连续 3 次连接 Java 后端失败: {e}") # 触发终极绝招:失败落库,把 Payload 塞进 Redis 死信队列,等待日后补偿 await save_failed_report_to_redis(user_id, session_id, payload)#