跨服务通信兜底机制-Java 回传失败无持久重试队列,报告可能静默丢失。

情况:

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)# 
相关推荐
满怀冰雪8 小时前
04-Paddle Tensor 基础:形状、数据类型、广播与索引
python·深度学习·神经网络·paddle
布朗克1688 小时前
Go 入门到精通-27-泛型编程
开发语言·golang·xcode·泛型编程
Tenifs8 小时前
关闭 IDEA 的英文单词拼写检查
java·intellij-idea
cui_ruicheng8 小时前
Python从入门到实战(十五):文件操作与目录管理
开发语言·python
captain3768 小时前
创建多线程程序
java·java-ee
野蛮人6号8 小时前
黑马天机学堂Day01-08.搭建项目环境-本地开发部署方式——不知道nacos的密码是什么
java·spring cloud·黑马程序员·天机学堂·黑马天机学堂
Roy_Sashulin8 小时前
灵杉Java编程平台第一章:安装
java·开发语言
名字还没想好☜8 小时前
Go 用 errgroup 管理并发子任务:错误收敛、取消传播与限流
开发语言·后端·golang·go·并发
xcLeigh8 小时前
Doubao-Seed-Evolving大模型接入教程|搭建全品类提示词+AI工具导航网页
前端·人工智能·python·ai·html·ai开发·豆包
不如语冰8 小时前
AI大模型入门-模块导入import
数据结构·人工智能·pytorch·python