<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>双栈架构 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%8F%8C%E6%A0%88%E6%9E%B6%E6%9E%84/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 20 Apr 2026 14:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%8F%8C%E6%A0%88%E6%9E%B6%E6%9E%84/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Spring Boot &#43; Python 双栈架构：AI 服务的工程化实践</title>
      <link>https://ftmao.com/blog/posts/java2python/</link>
      <pubDate>Mon, 20 Apr 2026 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/java2python/</guid>
      <description>&lt;h1 id=&#34;spring-boot--python-双栈架构ai-服务的工程化实践&#34;&gt;Spring Boot + Python 双栈架构：AI 服务的工程化实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我开发的方糖猫 AI 视频平台不是一个纯 Python 项目，也不是一个纯 Java 项目。它同时使用了 Spring Boot 3 和 Python 3.11：Java 负责业务系统和 API 服务，Python 负责 AI 推理、渲染和多媒体处理。这篇文章会分享我们为什么要这样做，以及如何解决两个技术栈之间的协作问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么必须双栈&#34;&gt;一、为什么必须双栈？&lt;/h2&gt;
&lt;p&gt;做一个 AI 视频生成平台，需要两类能力：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;能力类型&lt;/th&gt;
					&lt;th&gt;典型需求&lt;/th&gt;
					&lt;th&gt;更适合的栈&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;企业级业务系统&lt;/td&gt;
					&lt;td&gt;用户、订单、权限、支付、任务管理&lt;/td&gt;
					&lt;td&gt;Java / Spring Boot&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;AI 推理与多媒体&lt;/td&gt;
					&lt;td&gt;LLM 调用、Whisper、RAG、Remotion、FFmpeg&lt;/td&gt;
					&lt;td&gt;Python&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Java 的优势在于生态成熟、工程规范、团队熟悉；Python 的优势在于 AI 库丰富、模型推理和多媒体处理工具链完善。&lt;/p&gt;
&lt;p&gt;如果全用 Java，AI 库支持差，很多模型没有 Java SDK；如果全用 Python，业务系统的稳定性、事务处理、监控运维又很难做好。&lt;/p&gt;
&lt;p&gt;所以我们的选择是：&lt;strong&gt;Java 做业务底盘，Python 做 AI 引擎，两者通过清晰的接口协作&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二整体架构&#34;&gt;二、整体架构&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    FE[&amp;#34;前端 (Vue 3)&amp;#34;]
    GW[&amp;#34;API 网关 / BFF (Spring Boot)&amp;lt;br/&amp;gt;用户认证、请求路由、限流、任务状态查询、SSE 推送&amp;#34;]
    BIZ[&amp;#34;业务服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    SCHED[&amp;#34;任务调度服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    RES[&amp;#34;资源管理服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    AI[&amp;#34;AI Worker (Python)&amp;lt;br/&amp;gt;ReAct Agent、RAG 检索、Whisper、TTS、Remotion、FFmpeg&amp;#34;]
    STORE[&amp;#34;存储层：MySQL / Redis / OSS / ChromaDB&amp;#34;]

    FE --&amp;gt; GW
    GW --&amp;gt; BIZ
    GW --&amp;gt; SCHED
    GW --&amp;gt; RES
    SCHED -- &amp;#34;RocketMQ / Kafka&amp;#34; --&amp;gt; AI
    AI --&amp;gt; STORE&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;三职责划分&#34;&gt;三、职责划分&lt;/h2&gt;
&lt;h3 id=&#34;31-java-侧职责&#34;&gt;3.1 Java 侧职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API 网关&lt;/strong&gt;：统一入口、鉴权、限流、日志&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户与任务管理&lt;/strong&gt;：用户注册登录、任务创建、任务状态机、历史记录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;素材元数据管理&lt;/strong&gt;：素材上传、分类、标签、权限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付与计费&lt;/strong&gt;（如有）：订单、套餐、用量统计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务调度&lt;/strong&gt;：把渲染/生成任务发送到消息队列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时通知&lt;/strong&gt;：SSE 推送任务进度给用户&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-python-侧职责&#34;&gt;3.2 Python 侧职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI Agent 编排&lt;/strong&gt;：ReAct 任务规划、Tool 调用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG 检索&lt;/strong&gt;：Embedding 计算、向量库查询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音识别&lt;/strong&gt;：Whisper 转写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音合成&lt;/strong&gt;：TTS 配音&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频渲染&lt;/strong&gt;：Remotion 组件化渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后处理&lt;/strong&gt;：FFmpeg 转码、压缩、字幕、多分辨率输出&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-边界原则&#34;&gt;3.3 边界原则&lt;/h3&gt;
&lt;p&gt;我们定了几条原则来避免职责混乱：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Python 不直接暴露给用户&lt;/strong&gt;。所有用户请求都走 Java API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 只做无状态计算&lt;/strong&gt;。任务状态、用户信息、素材元数据都在 Java 侧维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信以异步为主&lt;/strong&gt;。AI 任务通常耗时较长，同步调用容易导致超时和耦合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据以 ID 为纽带&lt;/strong&gt;。Java 和 Python 之间不传大对象，只传任务 ID 和必要参数。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四服务间通信&#34;&gt;四、服务间通信&lt;/h2&gt;
&lt;h3 id=&#34;41-异步通信rocketmq&#34;&gt;4.1 异步通信：RocketMQ&lt;/h3&gt;
&lt;p&gt;AI 生成和渲染任务都走 RocketMQ：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Java 侧发送任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SendResult result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; rocketMQTemplate.&lt;span style=&#34;color:#a6e22e&#34;&gt;syncSend&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MessageBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;withPayload&lt;/span&gt;(taskMessage).&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Python 侧消费任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderConsumer&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        task &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; render_engine&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;process(task)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;异步通信的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解耦 Java 和 Python 服务&lt;/li&gt;
&lt;li&gt;Python Worker 可以独立扩缩容&lt;/li&gt;
&lt;li&gt;任务失败可以重试，不阻塞主流程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-同步通信http--grpc&#34;&gt;4.2 同步通信：HTTP / gRPC&lt;/h3&gt;
&lt;p&gt;少数场景需要同步调用，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询 RAG 素材（用户在前端点击「生成」前希望看到素材推荐）&lt;/li&gt;
&lt;li&gt;获取任务实时状态&lt;/li&gt;
&lt;li&gt;健康检查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们主要用 HTTP REST，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单直观，Java 和 Python 生态都完善&lt;/li&gt;
&lt;li&gt;调试方便，curl 即可测试&lt;/li&gt;
&lt;li&gt;对延迟要求不极端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;gRPC 我们也考虑过，但因为团队对 gRPC 熟悉度不高，且收益不明显，暂时没引入。&lt;/p&gt;
&lt;h3 id=&#34;43-数据一致性&#34;&gt;4.3 数据一致性&lt;/h3&gt;
&lt;p&gt;双栈架构下，任务状态是核心数据。我们用数据库作为唯一真相源：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;用户提交任务&amp;#34;] --&amp;gt; B[&amp;#34;Java 写入任务记录（status=pending）&amp;#34;]
    B --&amp;gt; C[&amp;#34;发送 RocketMQ 消息&amp;#34;]
    C --&amp;gt; D[&amp;#34;Python Worker 消费&amp;#34;]
    D --&amp;gt; E[&amp;#34;Python 更新任务状态（status=processing/completed/failed）&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Python Worker 更新数据库时，通过乐观锁防止并发冲突：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;UPDATE&lt;/span&gt; render_task
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SET&lt;/span&gt; status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;processing&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    worker_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;worker-01&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_at &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; NOW()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; task_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;xxx&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;AND&lt;/span&gt; status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;pending&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五数据库设计&#34;&gt;五、数据库设计&lt;/h2&gt;
&lt;h3 id=&#34;51-按服务分库还是分表&#34;&gt;5.1 按服务分库还是分表？&lt;/h3&gt;
&lt;p&gt;我们没有完全按微服务分库，而是按业务域分表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt;：用户信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;task&lt;/code&gt;：生成任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;material&lt;/code&gt;：素材元数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;：视频模板&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subscription&lt;/code&gt;：订阅/套餐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有表都在一个 MySQL 实例里，通过业务 ID 关联。这种方案在业务量不大时性价比最高，未来可按域拆分。&lt;/p&gt;
&lt;h3 id=&#34;52-python-侧如何访问数据库&#34;&gt;5.2 Python 侧如何访问数据库&lt;/h3&gt;
&lt;p&gt;Python 侧需要读取/更新任务状态，我们用的是 SQLAlchemy + 只读/写分离：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; sqlalchemy &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; create_engine
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; sqlalchemy.orm &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; sessionmaker
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;engine &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; create_engine(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mysql+pymysql://user:pass@host/db&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Session &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sessionmaker(bind&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;engine)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;update_task_status&lt;/span&gt;(task_id: str, status: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; Session() &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; session:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        session&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;UPDATE render_task SET status = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; WHERE task_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            (status, task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        session&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;commit()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;为了降低耦合，Python 侧只更新少量任务状态字段，不碰业务核心表。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六部署与运维&#34;&gt;六、部署与运维&lt;/h2&gt;
&lt;h3 id=&#34;61-容器化&#34;&gt;6.1 容器化&lt;/h3&gt;
&lt;p&gt;两个栈都用 Docker 打包：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Java 服务 Dockerfile&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;eclipse-temurin:17-jdk&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; target/app.jar /app.jar&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ENTRYPOINT&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/app.jar&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Python Worker Dockerfile&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;python:3.11-slim&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WORKDIR&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/app&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; requirements.txt .&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;RUN&lt;/span&gt; pip install -r requirements.txt&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; . .&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CMD&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;python&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;worker.py&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-cicd&#34;&gt;6.2 CI/CD&lt;/h3&gt;
&lt;p&gt;我们用一个简单的 GitHub Actions / Jenkins 流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码提交触发构建&lt;/li&gt;
&lt;li&gt;分别构建 Java 镜像和 Python 镜像&lt;/li&gt;
&lt;li&gt;推送到镜像仓库&lt;/li&gt;
&lt;li&gt;生产环境滚动更新&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;63-日志与链路追踪&#34;&gt;6.3 日志与链路追踪&lt;/h3&gt;
&lt;p&gt;双栈环境下，排查问题最大的难点是链路跨语言。我们的做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有请求携带 &lt;code&gt;trace_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Java 和 Python 都把 &lt;code&gt;trace_id&lt;/code&gt; 打到日志里&lt;/li&gt;
&lt;li&gt;用 ELK / Loki 做日志聚合，按 &lt;code&gt;trace_id&lt;/code&gt; 查询全链路&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; logging
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; logging&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;getLogger(__name__)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;info(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Start render&amp;#34;&lt;/span&gt;, extra&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;trace_id&amp;#34;&lt;/span&gt;: trace_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;: task_id})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;64-监控告警&#34;&gt;6.4 监控告警&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;监控项&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Java 服务&lt;/td&gt;
					&lt;td&gt;JVM 内存、GC、接口响应时间、错误率&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Python Worker&lt;/td&gt;
					&lt;td&gt;CPU/GPU 利用率、任务处理速率、队列堆积&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据库&lt;/td&gt;
					&lt;td&gt;连接数、慢查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息队列&lt;/td&gt;
					&lt;td&gt;堆积量、消费速率&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七踩过的坑&#34;&gt;七、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;71-序列化不一致&#34;&gt;7.1 序列化不一致&lt;/h3&gt;
&lt;p&gt;Java 侧用 Jackson，Python 侧用默认 json，时间格式、枚举值、空值处理不一致，导致接口对接时经常报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一用 ISO 8601 时间格式&lt;/li&gt;
&lt;li&gt;枚举值用字符串传递&lt;/li&gt;
&lt;li&gt;制定统一的 JSON Schema，用 Pydantic 和 Java Bean Validation 双向校验&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-python-服务内存泄漏&#34;&gt;7.2 Python 服务内存泄漏&lt;/h3&gt;
&lt;p&gt;Python Worker 长时间运行后，内存占用越来越高，尤其是加载大模型后。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Worker 处理一定数量任务后自动重启&lt;/li&gt;
&lt;li&gt;模型用单例模式加载，避免重复初始化&lt;/li&gt;
&lt;li&gt;显式清理大对象，调用 &lt;code&gt;gc.collect()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-跨语言异常处理&#34;&gt;7.3 跨语言异常处理&lt;/h3&gt;
&lt;p&gt;Python 异常信息传到 Java 侧变成一团乱码，前端无法友好展示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义统一的错误码体系&lt;/li&gt;
&lt;li&gt;Python 捕获异常后按规范封装错误响应&lt;/li&gt;
&lt;li&gt;Java 侧根据错误码映射为用户友好的提示&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-开发环境复杂度&#34;&gt;7.4 开发环境复杂度&lt;/h3&gt;
&lt;p&gt;开发时需要同时启动 Java 服务、Python Worker、MySQL、Redis、RocketMQ，新成员上手成本高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供 &lt;code&gt;docker-compose.yml&lt;/code&gt; 一键启动基础设施&lt;/li&gt;
&lt;li&gt;编写本地开发文档&lt;/li&gt;
&lt;li&gt;支持 Windows 开发环境降级（Python Worker 用 HTTP 模拟 RocketMQ）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八双栈架构的收益与代价&#34;&gt;八、双栈架构的收益与代价&lt;/h2&gt;
&lt;h3 id=&#34;81-收益&#34;&gt;8.1 收益&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方面&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;团队分工清晰&lt;/td&gt;
					&lt;td&gt;Java 工程师做业务，Python 工程师做 AI&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;技术选型合理&lt;/td&gt;
					&lt;td&gt;每个栈都用最合适的工具&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;稳定性高&lt;/td&gt;
					&lt;td&gt;业务系统独立演进，AI 故障不直接影响用户注册/支付&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩展灵活&lt;/td&gt;
					&lt;td&gt;Python Worker 可以根据渲染负载单独扩容&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;82-代价&#34;&gt;8.2 代价&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方面&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;运维复杂&lt;/td&gt;
					&lt;td&gt;两套构建、部署、监控体系&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;链路排查难&lt;/td&gt;
					&lt;td&gt;跨语言日志追踪需要额外设计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接口契约维护&lt;/td&gt;
					&lt;td&gt;Java 和 Python 之间需要稳定的 API 契约&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九什么情况下不要双栈&#34;&gt;九、什么情况下不要双栈？&lt;/h2&gt;
&lt;p&gt;虽然双栈适合我们，但不是所有项目都需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯业务系统&lt;/strong&gt;：全 Java 即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯模型研究/实验&lt;/strong&gt;：全 Python 即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队规模小&lt;/strong&gt;：双栈会增加认知负担&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MVP 早期&lt;/strong&gt;：先用最熟悉的栈跑通，后期再拆分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们是在产品验证基本跑通、团队有 5 人以上时才明确拆成双栈。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十总结&#34;&gt;十、总结&lt;/h2&gt;
&lt;p&gt;Spring Boot + Python 双栈不是最优架构，而是&lt;strong&gt;适合我们团队能力和业务形态的架构&lt;/strong&gt;。几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;职责边界要清晰&lt;/strong&gt;，否则双栈会变成双倍的混乱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步通信是主力&lt;/strong&gt;，同步只用于低延迟查询场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库是状态的唯一真相源&lt;/strong&gt;，避免两端各自维护状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志追踪要跨语言打通&lt;/strong&gt;，否则线上出问题很难排查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要为双栈而双栈&lt;/strong&gt;，MVP 阶段先用单栈验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 Java + Python 的混合架构，欢迎交流接口契约、部署和监控的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
