'use client'; import { useState } from 'react'; import { ArrowRight, Check, RotateCcw, SkipForward, Unplug, Database, Network, Server, Code2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; const states = { ready: { title: '等待请求', detail: '尚未创建任务。客户端准备提交一次笔记导出请求。', node: -1, deliveries: 0, writes: 0, }, persisted: { title: '事务提交', detail: 'API 在同一数据库事务写入任务与 outbox 记录。任务 ID 为 job-42。', node: 1, deliveries: 0, writes: 0, }, queued: { title: '消息已发布', detail: '发布器把 job-42 发送到队列。消息中的 ID 引用数据库任务记录。', node: 2, deliveries: 1, writes: 0, }, running: { title: 'Worker 执行', detail: 'Worker 取得执行资格,使用 job-42 作为本次操作的去重键。消息尚未 ACK。', node: 3, deliveries: 1, writes: 0, }, committed: { title: '业务事务已提交,尚未 ACK', detail: '结果和去重记录已原子提交。此时崩溃,队列仍可能再次投递消息。', node: 1, deliveries: 1, writes: 1, }, crashed: { title: 'Worker 在 ACK 前退出', detail: '已提交的数据库结果仍存在。未确认的消息在队列恢复机制触发后重新投递。', node: 2, deliveries: 1, writes: 1, }, redelivered: { title: '再次收到同一个任务', detail: '新的 Worker 检查 job-42 的已完成记录,返回已有结果,不再重复写入。', node: 3, deliveries: 2, writes: 1, }, done: { title: '确认消息,处理完成', detail: '消息被 ACK。正常执行时投递一次;经过故障恢复时可能投递两次。', node: 3, deliveries: 1, writes: 1, }, recovered: { title: '重复投递已处理', detail: '重复消息被 ACK。队列投递了两次,数据库副作用只提交一次。', node: 3, deliveries: 2, writes: 1, }, }; type State = keyof typeof states; const next: Partial> = { ready: 'persisted', persisted: 'queued', queued: 'running', running: 'committed', committed: 'done', crashed: 'redelivered', redelivered: 'recovered', }; export function TaskDeliveryDemo() { const [state, setState] = useState('ready'); const s = states[state]; return (
提交成功后,消息为什么还会重复? 状态模型演示 · 不连接真实服务
{[ { name: 'API', icon: Code2 }, { name: 'PostgreSQL', icon: Database }, { name: '消息队列', icon: Network }, { name: 'Worker', icon: Server }, ].map((n, i) => (
{n.name} {i < 3 && }
))}

{s.title}

{s.detail}

任务 ID job-42 消息投递 {s.deliveries} 业务写入 {s.writes}
{!next[state] && }

本例假设业务结果与去重记录处于同一个数据库事务,并有唯一约束。发送邮件、调用支付等外部副作用需要单独设计幂等协议。队列恢复时间取决于 broker、ACK 和 visibility timeout 配置。

); }