| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- '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<Record<State, State>> = {
- ready: 'persisted',
- persisted: 'queued',
- queued: 'running',
- running: 'committed',
- committed: 'done',
- crashed: 'redelivered',
- redelivered: 'recovered',
- };
- export function TaskDeliveryDemo() {
- const [state, setState] = useState<State>('ready');
- const s = states[state];
- return (
- <div className="demo delivery-demo">
- <div className="demo-top">
- <span className="demo-label">提交成功后,消息为什么还会重复?</span>
- <span className="simulation">状态模型演示 · 不连接真实服务</span>
- </div>
- <div className="delivery-nodes">
- {[
- { name: 'API', icon: Code2 },
- { name: 'MySQL', icon: Database },
- { name: '消息队列', icon: Network },
- { name: 'Worker', icon: Server },
- ].map((n, i) => (
- <div className={s.node === i ? 'current' : ''} key={n.name}>
- <n.icon size={22} />
- <span>{n.name}</span>
- {i < 3 && <ArrowRight size={13} />}
- </div>
- ))}
- </div>
- <div className="demo-result" aria-live="polite">
- <div>
- <h3>{s.title}</h3>
- <p>{s.detail}</p>
- </div>
- </div>
- <div className="delivery-counters">
- <span>
- 任务 ID <strong>job-42</strong>
- </span>
- <span>
- 消息投递 <strong>{s.deliveries}</strong>
- </span>
- <span>
- 业务写入 <strong>{s.writes}</strong>
- </span>
- </div>
- <div className="demo-controls">
- <Button
- variant="outline"
- disabled={!next[state]}
- onClick={() => setState(next[state]!)}
- >
- <SkipForward />
- {state === 'ready' ? '提交任务' : '推进一步'}
- </Button>
- <Button
- variant="ghost"
- disabled={state !== 'committed'}
- onClick={() => setState('crashed')}
- >
- <Unplug />
- 提交后崩溃
- </Button>
- <Button
- variant="ghost"
- onClick={() => setState('ready')}
- aria-label="重置任务投递演示"
- >
- <RotateCcw />
- </Button>
- {!next[state] && <Check size={16} />}
- </div>
- <p className="demo-footnote">
- 本例假设业务结果与去重记录处于同一个数据库事务,并有唯一约束。发送通知等外部副作用需要单独设计幂等协议。队列恢复时间取决于
- broker、ACK 和 visibility timeout 配置。
- </p>
- </div>
- );
- }
|