跨服务通信兜底机制-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)# 
相关推荐
myenjoy_114 小时前
MQTT 与 Sparkplug B——从车间到云端的最后一公里
网络·python
z落落14 小时前
C# 四种特殊类:抽象类、密封类、静态类、部分类
开发语言·c#
摇滚侠14 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown15 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
折哥的程序人生 · 物流技术专研15 小时前
Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?
java·设计模式·架构·原型模式·单一职责原则
装不满的克莱因瓶15 小时前
基于 OpenResty 扩展开发实现动态服务注册与发现能力
java·开发语言·架构·openresty
程序员小羊!15 小时前
06Java 异常机制与常用类
java
颜酱15 小时前
LangChain 输出解析器:把模型回复变成你要的数据
python·langchain
2401_8734794015 小时前
企业安全运营中,如何用IP离线库提前发现失陷主机?三步实现风险画像
网络·数据库·python·tcp/ip·ip
weixin_5231853216 小时前
Java基础知识总结(四):引用数据类型与参数传递机制
java·开发语言·python