持久化执行的艺术:Temporal 与 AI Agent 编排

从 Redis 状态机到 Event Sourcing:分布式任务编排的演进之路

January 9, 2026·21 min read·Yimin
#Temporal#分布式#工作流#AI Agent#Event Sourcing

Temporal 让你像写单机程序一样写分布式工作流。

🎯 问题背景:分布式任务编排的困境

一个典型场景

想象你正在构建一个 AI Agent 系统,需要完成以下流程:

┌─────────────────────────────────────────────────────────────┐
│                    AI Agent 执行流程                         │
│                                                             │
│   [1. 分析任务] → [2. 等待用户确认] → [3. 执行操作]          │
│         ↓              ↓                    ↓               │
│      调用 LLM       可能等几小时          调用外部 API        │
│                                             ↓               │
│                                      [4. 汇总结果]          │
└─────────────────────────────────────────────────────────────┘

看起来简单,但魔鬼藏在细节里:

  • 步骤 2 等待确认时:进程在干什么?占着内存等?
  • 等待过程中服务重启了:怎么恢复到"等待确认"状态?
  • 步骤 3 失败了:重试?回滚?通知用户?
  • 如果有子任务:子任务失败如何影响主任务?

传统方案的痛点

方案实现思路问题
数据库轮询定时扫描 pending 任务延迟高、数据库压力大
消息队列每步发消息触发下一步状态分散、恢复困难
状态机 + Redis存储当前状态,代码判断恢复逻辑复杂、状态可能丢失
自研调度系统从头造轮子维护成本高、容易有 bug

核心难题可以归结为四个:

┌─────────────────────────────────────────────────────────────┐
│                    四大难题                                  │
│                                                             │
│   1. 状态持久化        2. 长时间等待                         │
│      进程崩溃后           等待人工审批时                      │
│      如何恢复?           资源如何释放?                      │
│                                                             │
│   3. 嵌套编排          4. 分布式协调                         │
│      子任务、孙任务       多个 Worker                        │
│      如何管理?           如何协同?                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

🏛️ Temporal 架构概览

Temporal 是一个开源的持久化执行平台,最初由 Uber 开发(前身是 Cadence),现在是独立公司维护的项目。

整体架构

┌─────────────────────────────────────────────────────────────┐
│                        客户端应用                            │
│                  (启动工作流、发送信号、查询状态)              │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                    Temporal Server                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │  Frontend   │  │  Matching   │  │      History        │ │
│  │  (API 网关) │  │  (任务分发) │  │  (事件存储/重放)    │ │
│  └─────────────┘  └─────────────┘  └─────────────────────┘ │
│                           │                                 │
│                    ┌──────┴──────┐                         │
│                    │  持久化存储  │                         │
│                    │ (MySQL/PG/  │                         │
│                    │  Cassandra) │                         │
│                    └─────────────┘                         │
└─────────────────────────┬───────────────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │ Worker 1 │    │ Worker 2 │    │ Worker N │
    │ (无状态) │    │ (无状态) │    │ (无状态) │
    └──────────┘    └──────────┘    └──────────┘

核心概念

概念类比作用
Workflow状态机/流程图定义编排逻辑,决定"做什么"
Activity函数调用执行实际业务代码(调 API、写数据库)
Worker工人从队列拉取任务并执行,完全无状态
Signal消息/事件外部向工作流发送输入(如用户确认)
Query查询接口获取工作流当前状态
Timer定时器等待一段时间后继续

关键设计原则

Worker 无状态,Server 有状态。

这意味着:

  • Worker 可以随时重启、扩缩容
  • 所有状态都在 Temporal Server(通过数据库持久化)
  • 任何 Worker 都可以执行任何任务

🔮 核心原理:Event Sourcing 与确定性重放

这是理解 Temporal "魔法"的关键。

传统状态存储 vs Event Sourcing

传统方式:存储快照

数据库/Redis 中存储:
{
  "task_id": "abc-123",
  "current_step": 3,
  "status": "waiting_approval",
  "data": { ... }
}

问题:

  • 只知道"现在在哪",不知道"怎么来的"
  • 恢复时需要大量 if/else 判断
  • 历史不可追溯

Event Sourcing:存储事件

Event History:
┌─────────────────────────────────────────────────────────────┐
│  [1] WorkflowExecutionStarted                               │
│      { input: { goal: "分析市场报告" } }                    │
│                                                             │
│  [2] ActivityTaskScheduled                                  │
│      { activityType: "AnalyzeTask" }                       │
│                                                             │
│  [3] ActivityTaskCompleted                                  │
│      { result: "分析完成,建议..." }                        │
│                                                             │
│  [4] TimerStarted                                           │
│      { duration: "24h", reason: "等待用户确认" }            │
│                                                             │
│  [5] SignalReceived                                         │
│      { signalName: "userConfirm", data: { approved: true }} │
│                                                             │
│  [6] ActivityTaskScheduled                                  │
│      { activityType: "ExecuteTask" }                       │
│                                                             │
│  ...                                                        │
└─────────────────────────────────────────────────────────────┘

核心思想:不存储状态,存储所有发生过的事件。状态可以通过重放事件推导出来。

重放机制详解

假设工作流执行到第 4 步时,Worker 进程崩溃了:

┌─────────────────────────────────────────────────────────────┐
│                    崩溃前的执行                              │
│                                                             │
│  Worker A 执行:                                            │
│  ─────────────                                              │
│  [1] 启动工作流    → 写入事件 [1]                           │
│  [2] 调用 Activity → 写入事件 [2]                           │
│  [3] Activity 完成 → 写入事件 [3]                           │
│  [4] 启动 Timer    → 写入事件 [4]                           │
│      💥 Worker A 崩溃!                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    恢复后的重放                              │
│                                                             │
│  Worker B 接手(可以是任何 Worker):                        │
│  ─────────────────────────────────                          │
│  重放事件 [1] → 跳过(已完成)                              │
│  重放事件 [2] → 跳过(已完成)                              │
│  重放事件 [3] → 跳过(已完成)                              │
│  重放事件 [4] → 发现 Timer 还在等待                         │
│                                                             │
│  继续等待 Timer...                                          │
│  (或者发现 Timer 已到期 / Signal 已收到,继续执行)         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键点:

  1. 事件先写入数据库,才算完成——不会丢失
  2. 重放时,已完成的步骤直接跳过——不会重复执行
  3. 任何 Worker 都可以重放——无需特定进程

确定性要求

为什么 Temporal 要求工作流代码必须是确定性的

┌─────────────────────────────────────────────────────────────┐
│                    确定性 vs 非确定性                        │
│                                                             │
│   ✅ 允许                        ❌ 不允许                   │
│   ─────────                      ───────────                │
│   • 调用 Activity               • 直接调用外部 API          │
│   • 使用 Workflow Timer         • 使用 time.Now()           │
│   • 使用 Workflow Random        • 使用 math/rand            │
│   • 读取 Workflow 输入          • 读取环境变量              │
│   • 条件分支(基于输入/结果)    • 条件分支(基于当前时间)   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

原因:重放时必须产生相同的执行路径。

想象一下,如果工作流代码是:

如果当前时间是早上 → 执行路径 A
否则 → 执行路径 B

首次执行时是早上,走了路径 A。 崩溃后重放时是下午,走了路径 B。

这就完全乱了!Temporal 无法正确恢复状态。

所以:所有"不确定"的操作(时间、随机数、外部调用)都必须通过 Activity 或 Workflow API 来做,这样 Temporal 可以记录结果,重放时直接使用记录的结果。

与 Redis 快照方案对比

维度Redis 快照Temporal Event Sourcing
存什么当前状态(JSON)所有事件(Event History)
空间占用小(只有最新状态)大(但可以归档/清理)
恢复速度快(直接读取)需要重放(但有优化)
历史追溯❌ 无法追溯✅ 完整审计日志
数据一致性可能丢失(Redis 持久化策略)强一致(数据库事务)
调试能力弱(只能看当前状态)强(可以看每一步)
恢复逻辑需要手写 if/else自动(重放机制)

⏸️ 中断与恢复机制深度解析

传统方案的中断恢复为什么难?

问题一:状态保存时机

┌─────────────────────────────────────────────────────────────┐
│   保存太频繁?                    保存太少?                 │
│   ──────────                      ──────────                │
│   • 每一步都存 Redis             • 只在关键点存              │
│   • 性能差                        • 中间步骤可能丢失         │
│   • 网络开销大                    • 恢复时状态不完整         │
└─────────────────────────────────────────────────────────────┘

问题二:恢复逻辑复杂

恢复时需要判断:
├── 状态是 "分析中"?→ 重新调用 LLM
├── 状态是 "等待确认"?→ 继续等待
├── 状态是 "执行中"?→ 
│   ├── 子任务 A 完成了吗?
│   ├── 子任务 B 完成了吗?
│   └── 需要回滚吗?
└── ...

状态越多,恢复逻辑越像意大利面条 🍝

问题三:等待时的资源占用

等待用户确认(可能几小时/几天):

方案 A: 进程阻塞等待
  → 一个任务占用一个线程/协程
  → 100 个等待中的任务 = 100 个闲置资源

方案 B: 轮询数据库
  → 定时查询 "有没有用户确认"
  → 延迟 = 轮询间隔
  → 数据库压力大

Temporal 的优雅解决方案

等待时发生了什么?

┌─────────────────────────────────────────────────────────────┐
│  时间线                                                     │
│  ───────                                                    │
│                                                             │
│  T1: 工作流执行到 "等待信号"                                │
│      │                                                      │
│      ├─→ 写入事件: WorkflowTaskCompleted                   │
│      │   (记录 "我在等待信号")                              │
│      │                                                      │
│      └─→ Worker 完成当前任务,释放资源 ✓                   │
│          (没有任何进程在"等待")                           │
│                                                             │
│  ────────── 可能过了几小时/几天 ──────────                  │
│                                                             │
│  T2: 用户发送确认信号                                       │
│      │                                                      │
│      ├─→ Server 收到信号                                   │
│      │                                                      │
│      ├─→ 写入事件: SignalReceived                          │
│      │                                                      │
│      └─→ Server 创建新的 Workflow Task                     │
│          放入 Task Queue                                    │
│                                                             │
│  T3: 某个 Worker 拉取任务                                  │
│      │                                                      │
│      ├─→ 重放 Event History                                │
│      │                                                      │
│      ├─→ 发现 SignalReceived 事件                          │
│      │                                                      │
│      └─→ 继续执行后续逻辑 ✓                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键洞察:

  1. 等待期间没有任何进程/线程在占用资源
  2. 状态完全由 Event History 保持
  3. 任何 Worker 都可以恢复执行
  4. 等待 1 秒和等待 1 年,对系统来说没有区别

Signal 机制 vs 传统轮询

维度传统轮询Temporal Signal
实现定时查询数据库事件驱动
延迟取决于轮询间隔(秒级)毫秒级
等待时资源消耗(轮询进程)零消耗
扩展性任务越多,数据库压力越大线性扩展
代码复杂度需要管理轮询逻辑直接 "等待信号"

超时与重试策略

Temporal 内置了完善的超时和重试机制:

超时类型:

超时类型含义场景
Start-to-Close单次执行最长时间Activity 调用超时
Schedule-to-Start排队等待最长时间Worker 繁忙时
Schedule-to-Close从排队到完成最长时间端到端超时
Heartbeat心跳间隔长时间运行的 Activity

重试策略:

┌─────────────────────────────────────────────────────────────┐
│                    指数退避重试                              │
│                                                             │
│   第 1 次失败 → 等待 1 秒  → 重试                          │
│   第 2 次失败 → 等待 2 秒  → 重试                          │
│   第 3 次失败 → 等待 4 秒  → 重试                          │
│   第 4 次失败 → 等待 8 秒  → 重试                          │
│   ...                                                       │
│   达到最大次数 → 标记失败,可触发补偿逻辑                    │
│                                                             │
│   配置项:                                                  │
│   • 初始间隔(InitialInterval)                            │
│   • 退避系数(BackoffCoefficient)                         │
│   • 最大间隔(MaximumInterval)                            │
│   • 最大重试次数(MaximumAttempts)                        │
│   • 不重试的错误类型(NonRetryableErrors)                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

🌳 递归式子孙任务编排

为什么需要嵌套工作流?

复杂任务往往需要分解:

┌─────────────────────────────────────────────────────────────┐
│                    场景:批量数据处理                        │
│                                                             │
│   主任务:处理 1000 个用户的年度报告                        │
│   │                                                         │
│   ├─ 子任务 1:处理用户 A                                  │
│   │   ├─ 孙任务:拉取交易记录                              │
│   │   ├─ 孙任务:计算统计数据                              │
│   │   └─ 孙任务:生成 PDF                                  │
│   │                                                         │
│   ├─ 子任务 2:处理用户 B                                  │
│   │   └─ ...                                                │
│   │                                                         │
│   └─ 子任务 N:...                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

如果用传统方式:

  • 需要手动管理父子关系
  • 需要自己实现 ID 生成和关联
  • 子任务失败时,父任务怎么知道?
  • 要取消整个任务树,怎么遍历?

Temporal 的 Child Workflow

┌─────────────────────────────────────────────────────────────┐
│                     Parent Workflow                         │
│                     WorkflowID: parent-123                  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  独立的 Event History                                 │  │
│  │  [1] WorkflowStarted                                  │  │
│  │  [2] ChildWorkflowExecutionStarted { id: child-A }    │  │
│  │  [3] ChildWorkflowExecutionStarted { id: child-B }    │  │
│  │  [4] ChildWorkflowExecutionCompleted { id: child-A }  │  │
│  │  [5] ChildWorkflowExecutionCompleted { id: child-B }  │  │
│  │  [6] WorkflowCompleted                                │  │
│  └──────────────────────────────────────────────────────┘  │
│                         │                                   │
│           ┌─────────────┴─────────────┐                    │
│           ▼                           ▼                    │
│    ┌────────────────┐          ┌────────────────┐         │
│    │  Child A       │          │  Child B       │         │
│    │  id: child-A   │          │  id: child-B   │         │
│    │                │          │                │         │
│    │  独立 History  │          │  独立 History  │         │
│    │  独立 Signal   │          │  独立 Signal   │         │
│    └───────┬────────┘          └────────────────┘         │
│            │                                               │
│    ┌───────┴───────┐                                       │
│    ▼               ▼                                       │
│ ┌───────┐     ┌───────┐                                    │
│ │Grand 1│     │Grand 2│  ← 孙任务                          │
│ │独立 WF│     │独立 WF│                                    │
│ └───────┘     └───────┘                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

子任务的独立性

每个子工作流都是完全独立的:

特性说明
独立 WorkflowID每个子任务有唯一标识,可单独查询
独立 Event History状态完全隔离,互不影响
独立 Signal 通道可以单独给子任务发送信号
独立超时/重试子任务有自己的超时和重试策略
独立 Query可以单独查询子任务状态

子任务中断如何影响父任务?

场景:孙任务等待人工审批

┌─────────────────────────────────────────────────────────────┐
│  时间线                                                     │
│  ───────                                                    │
│                                                             │
│  T1: 主任务启动,创建子任务 A                               │
│                                                             │
│  T2: 子任务 A 启动,创建孙任务 X                            │
│                                                             │
│  T3: 孙任务 X 执行到 "等待审批"                             │
│      ├─→ 孙任务 X:暂停,等待 Signal                       │
│      ├─→ 子任务 A:暂停,等待孙任务 X 完成                 │
│      └─→ 主任务:暂停,等待子任务 A 完成                   │
│                                                             │
│      ⚡ 此时:没有任何 Worker 进程在等待!                   │
│         所有状态都在各自的 Event History 中                 │
│                                                             │
│  ────────── 一段时间后 ──────────                           │
│                                                             │
│  T4: 用户发送 Signal 给孙任务 X                             │
│      (注意:是发给孙任务,不是主任务)                      │
│                                                             │
│  T5: 孙任务 X 收到 Signal,继续执行,完成                   │
│                                                             │
│  T6: 子任务 A 发现孙任务 X 完成,继续执行,完成             │
│                                                             │
│  T7: 主任务发现子任务 A 完成,继续执行                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

父任务的控制策略

策略行为适用场景
等待完成父任务阻塞直到子任务完成子任务结果影响后续逻辑
分离执行父任务不等待,子任务独立运行Fire-and-forget
取消传播父任务取消时,自动取消所有子任务整体任务取消
错误处理子任务失败时,父任务可选择:忽略/重试/失败/补偿容错设计

与传统方案对比

维度传统方案(手动实现)Temporal Child Workflow
状态管理需要手动维护父子关系表框架自动管理
ID 生成需要自己设计命名规则自动生成 / 可自定义
信号路由需要自己实现分发逻辑每个工作流独立通道
取消传播需要手动遍历取消配置 ParentClosePolicy
嵌套深度深度越深越难管理理论上无限嵌套
可观测性需要自建监控系统Temporal UI 原生支持
恢复复杂度指数级增长各自独立恢复

🔐 并发控制:为什么不需要分布式锁?

这是一个常被问到的问题:多个 Worker 同时运行,状态怎么保证一致?不需要加锁吗?

Temporal 的并发控制机制

┌─────────────────────────────────────────────────────────────┐
│              Temporal 如何避免竞态条件                       │
│                                                             │
│   1. 工作流执行是"单线程"的                                 │
│      ────────────────────────                               │
│      同一个 WorkflowID 的工作流,在任意时刻                  │
│      只有一个 Workflow Task 在执行                          │
│      Server 保证任务不会被重复分发                          │
│                                                             │
│   2. Server 通过数据库保证互斥                              │
│      ──────────────────────────                             │
│      • 乐观锁(版本号校验)                                 │
│      • 数据库事务(原子写入)                               │
│      • Shard 分片(减少锁竞争)                             │
│                                                             │
│   3. 多个 Signal 同时到达?                                 │
│      ─────────────────────                                  │
│      Server 会排队处理                                      │
│      工作流代码看到的是顺序的事件流                         │
│      不会乱序,不会丢失                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

与传统分布式锁对比

场景传统方案(Redis 分布式锁)Temporal
同一任务被多个 Worker 抢需要 SETNX 抢锁Server 只分发给一个
状态更新冲突需要乐观锁/悲观锁Event 是 append-only
多个请求同时修改需要手动加锁工作流单线程执行
Signal 并发需要队列或锁Server 自动排队
锁过期问题可能导致重复执行版本号机制避免

传统分布式锁的经典问题

┌─────────────────────────────────────────────────────────────┐
│   Redis 分布式锁的隐患                                       │
│                                                             │
│   问题 1:锁过期但任务还没完成                               │
│   ─────────────────────────────                             │
│   Worker A 持锁执行任务                                      │
│   发生 GC 或网络延迟,执行变慢                              │
│   锁过期,Worker B 获得锁                                   │
│   Worker A 恢复继续执行...                                  │
│   💥 两个 Worker 同时在执行同一个任务!                     │
│                                                             │
│   问题 2:时钟漂移                                          │
│   ────────────────                                          │
│   Worker A 的系统时钟比 Redis 快 5 秒                       │
│   Worker A 认为锁还有效                                     │
│   实际上 Redis 那边已经过期了...                            │
│   💥 数据不一致!                                           │
│                                                             │
│   问题 3:网络分区                                          │
│   ────────────────                                          │
│   Worker A 获得锁后,与 Redis 网络断开                      │
│   无法续期,锁被 Worker B 获得                              │
│   网络恢复后,Worker A 认为自己还持有锁...                  │
│   💥 脑裂问题!                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Temporal 如何避免这些问题

┌─────────────────────────────────────────────────────────────┐
│   Temporal 的解决方案                                        │
│                                                             │
│   1. 不是"锁",是"租约 + 版本号"                            │
│      ─────────────────────────                              │
│      Worker 拿到的是带版本号的任务                          │
│      提交结果时校验版本号                                   │
│      版本不匹配?提交失败,Server 会重新调度                │
│      失败了也没关系,重放机制保证正确性                     │
│                                                             │
│   2. Server 是唯一的协调者                                  │
│      ────────────────────────                               │
│      不依赖 Worker 的本地时钟                               │
│      所有决策由 Server 做出                                 │
│      Worker 只是"执行者"                                    │
│                                                             │
│   3. 幂等性 + 重放 = 最终一致                               │
│      ─────────────────────────                              │
│      即使出现异常情况                                       │
│      重放机制保证:已完成的步骤不会重复执行                 │
│      最终状态一定是正确的                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Signal 的顺序保证

当多个 Signal 同时发送时会发生什么?

10:00:00.001 - 用户 A 发送 Signal "confirm"
10:00:00.002 - 用户 B 发送 Signal "cancel"

Server 处理:
┌─────────────────────────────────────┐
│  Signal 队列(按到达顺序)          │
│  [1] confirm (10:00:00.001)        │
│  [2] cancel  (10:00:00.002)        │
└─────────────────────────────────────┘

工作流看到的事件:
[N]   SignalReceived { name: "confirm" }
[N+1] SignalReceived { name: "cancel" }

✅ 顺序确定,不会乱序
✅ 不会丢失任何 Signal
✅ 工作流代码可以按顺序处理

小结

Temporal 的哲学:把并发控制的复杂性交给基础设施,业务代码不需要关心锁。

你不需要关心Temporal 帮你处理
任务会不会被重复执行?Server 保证只分发一次
状态更新会不会冲突?版本号 + 重放机制
Signal 会不会乱序?Server 排队处理
Worker 崩溃了怎么办?自动重新调度

⚖️ 与 Redis PubSub 方案的全面对比

在实际项目中,我们也实现过基于 Redis 的编排系统。这里做一个详细对比:

架构对比

┌─────────────────────────────────────────────────────────────┐
│                    Redis PubSub 方案                        │
│                                                             │
│  ┌─────────┐    ┌─────────┐    ┌─────────────────────────┐ │
│  │ API 1   │    │ API 2   │    │      Redis              │ │
│  │(进程1)  │    │(进程2)  │    │  • State (JSON 快照)    │ │
│  └────┬────┘    └────┬────┘    │  • PubSub 频道          │ │
│       │              │         │  • running 标志         │ │
│       └──────────────┴─────────┤  • subscribers 集合     │ │
│                                └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                        Temporal 方案                        │
│                                                             │
│  ┌─────────┐    ┌─────────┐    ┌─────────────────────────┐ │
│  │ Worker1 │    │ Worker2 │    │   Temporal Server       │ │
│  │(无状态) │    │(无状态) │    │  • Event History (DB)   │ │
│  └────┬────┘    └────┬────┘    │  • Task Queue           │ │
│       │              │         │  • Signal Queue         │ │
│       └──────────────┴─────────┤  • Timer Service        │ │
│                                └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

核心差异

维度Redis PubSubTemporal
状态存储JSON 快照(当前状态)Event History(所有事件)
恢复方式读取 JSON + if/else 判断自动重放
子任务支持需要手动实现原生 Child Workflow
实时推送✅ PubSub 原生支持需要额外实现
历史追溯❌ 只有最新状态✅ 完整历史
故障恢复可能丢失状态强一致保证
运维复杂度低(只需 Redis)中(需要 Temporal Server)
学习曲线

实时推送能力对比

Redis PubSub 的优势:

┌─────────────────────────────────────────────────────────────┐
│                    真正的实时推送                            │
│                                                             │
│   Worker 执行 Activity 时:                                 │
│   ─────────────────────────                                 │
│   1. 调用 LLM,收到一个 chunk                               │
│   2. 立即 Publish 到 Redis                                  │
│   3. 所有订阅者(包括其他进程)立即收到                      │
│   4. 前端实时显示                                           │
│                                                             │
│   延迟:毫秒级                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Temporal 需要额外实现:

Temporal 本身是为持久化设计的,不是为实时推送设计的。

要实现实时推送,可以:

  1. 在 Activity 里调用 Redis PubSub 推送
  2. 使用 Query 轮询(有延迟)
  3. 使用 Temporal UI(仅限调试)

混合架构建议:

┌─────────────────────────────────────────────────────────────┐
│                    混合架构                                  │
│                                                             │
│   Temporal                      Redis PubSub                │
│   ┌─────────────┐              ┌─────────────────┐         │
│   │ 编排逻辑    │              │ 实时推送        │         │
│   │ 状态持久化  │──Activity───▶│ 流式输出        │         │
│   │ 中断恢复    │   内调用     │ 前端更新        │         │
│   │ 子任务管理  │              │                 │         │
│   └─────────────┘              └─────────────────┘         │
│                                                             │
│   各取所长:                                                 │
│   • Temporal 负责"可靠性"                                   │
│   • Redis 负责"实时性"                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

🚀 Temporal 的更多能力

除了上述核心功能,Temporal 还提供:

工作流版本管理

当你需要修改工作流逻辑时,已经在运行的工作流怎么办?

Temporal 支持版本标记:旧版本的工作流继续用旧逻辑执行,新启动的工作流用新逻辑。

Saga 模式(分布式事务补偿)

┌─────────────────────────────────────────────────────────────┐
│                    订单处理 Saga                             │
│                                                             │
│   正向流程:                                                 │
│   [扣减库存] → [创建订单] → [扣款] → [发送通知]             │
│                                                             │
│   如果"扣款"失败:                                          │
│   [恢复库存] ← [取消订单] ← [失败]                          │
│                                                             │
│   Temporal 自动管理补偿逻辑                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

定时任务(Schedule)

比 Cron 更强大:

  • 支持 Cron 表达式
  • 支持间隔触发
  • 支持暂停/恢复
  • 有完整的执行历史

搜索属性(Search Attributes)

给工作流打标签,支持复杂查询:

查询:Status = "processing" AND CustomerId = "cust-123" AND OrderTotal > 100

长时间运行(Continue-as-New)

当 Event History 太大时,可以"重启"工作流,保留状态但清空历史。


📊 何时选择 Temporal?

适合的场景

场景原因
AI Agent 编排多步骤、需要人工介入、可能失败需重试
订单处理流程长时间运行、需要补偿逻辑
审批工作流等待时间不确定、需要状态持久化
数据管道多阶段处理、需要重试和监控
微服务编排跨服务调用、需要事务一致性
定时任务比 Cron 更可靠、有执行历史

不太适合的场景

场景原因替代方案
超低延迟(<10ms)有调度开销直接调用
简单任务队列过于复杂Redis Queue / SQS
纯实时流式输出不是设计目标WebSocket / SSE
短生命周期任务不需要持久化普通函数调用

部署选择

选项适合场景注意事项
Docker Compose开发/测试不适合生产
Kubernetes生产环境需要运维能力
Temporal Cloud不想管运维按量付费

🎯 总结

Temporal 的核心价值

  1. 持久化执行:进程崩溃?自动恢复,一行代码都不用改
  2. 长时间等待:等待用户审批时,零资源消耗
  3. 复杂编排:子孙任务嵌套,框架自动管理
  4. 可观测性:完整的执行历史,调试神器

设计哲学

困难的事情(状态管理、分布式协调、故障恢复)交给基础设施,让业务代码保持简单直接

一句话总结

Temporal 让你像写单机程序一样写分布式工作流,像调试单进程一样调试跨服务流程。

对于 AI Agent 这种需要多步骤执行、人工介入、长时间运行的场景,Temporal 提供了一个优雅且可靠的解决方案。


📚 延伸阅读