首页 › tutorials › Claude Code事件驱动架构:队列到异步Worker

Claude Code事件驱动架构实战:消息队列到异步Worker全流程

学习目标:掌握使用Claude Code构建事件驱动架构(EDA)的完整流程,涵盖Redis消息队列、BullMQ任务系统、事件总线设计、异步Worker模式,以及生产级监控面板。读完本教程,你能独立用Claude Code搭建一个高可用的异步任务处理平台。

适用人群:有Node.js/TypeScript基础、希望用AI辅助构建分布式后端系统的中高级开发者。


一、前置条件

工具 版本要求 用途
Claude Code 最新版 (≥1.0) AI编程助手
Node.js ≥20.x 运行时环境
Redis ≥7.0 消息队列与缓存
Docker ≥24.x 容器化Redis
TypeScript ≥5.3 类型安全

确保已安装Claude Code并完成认证:

claude --version
# 输出类似: 1.x.x

启动Redis服务:

docker run -d --name redis-eda \
  -p 6379:6379 \
  -v redis-data:/data \
  redis:7-alpine redis-server --appendonly yes

二、事件驱动架构核心概念

2.1 为什么需要事件驱动?

传统请求-响应模型在以下场景力不从心:

graph LR
    subgraph 传统同步模型
        A[客户端] -->|HTTP请求| B[API服务]
        B -->|同步调用| C[邮件服务]
        B -->|同步调用| D[报表服务]
        B -->|等待...| E[返回响应]
        style E fill:#f96,stroke:#333
    end

    subgraph 事件驱动模型
        F[客户端] -->|HTTP请求| G[API服务]
        G -->|发布事件| H[事件总线]
        H -->|异步| I[邮件Worker]
        H -->|异步| J[报表Worker]
        G -->|立即返回| K[响应✓]
        style K fill:#9f6,stroke:#333
    end

2.2 技术选型对比

方案 优势 劣势 适用场景
Redis Pub/Sub 极低延迟、简单 不持久化、无ACK 实时通知、缓存失效
Redis Streams 持久化、消费者组 运维复杂度中等 中小规模事件流
BullMQ 重试、延迟、优先级 依赖Redis 任务队列(本教程重点)
Kafka 高吞吐、分区 基础设施重 大规模事件流
RabbitMQ 灵活路由、ACK 吞吐量中等 企业集成

本教程选择BullMQ:它在简单性和功能性之间取得最佳平衡,适合90%的中小型项目,且与Claude Code的配合最为顺畅。


三、项目初始化

3.1 创建项目骨架

mkdir event-driven-platform && cd event-driven-platform
claude init

在交互式引导中选择TypeScript项目,Claude Code会自动生成基础配置。

3.2 配置CLAUDE.md

在项目根目录创建CLAUDE.md,这是Claude Code理解项目上下文的关键:

# Event-Driven Platform

## Architecture
- Event-driven architecture using BullMQ + Redis
- TypeScript strict mode
- Pattern: Producer → Queue → Worker → Event Bus

## Tech Stack
- Runtime: Node.js 20+ with ESM modules
- Queue: BullMQ 5.x backed by Redis 7
- Validation: Zod schemas
- Logging: Pino structured logging
- Testing: Vitest

## Project Structure

src/ ├── producers/ # 事件生产者 ├── workers/ # 异步Worker ├── events/ # 事件定义与总线 ├── queues/ # 队列配置 ├── monitoring/ # 监控面板 └── shared/ # 共享类型与工具

## Rules
- 所有事件必须使用Zod schema验证
- Worker必须实现优雅关闭(graceful shutdown)
- 使用structured logging,禁止console.log
- 错误处理使用Result模式,不抛异常

3.3 安装依赖

让Claude Code帮你安装:

claude "安装以下依赖:bullmq ioredis zod pino pino-pretty dotenv,以及开发依赖 typescript @types/node tsx vitest"

Claude Code会生成精确的安装命令并执行。


四、核心模块实现

4.1 共享类型与Redis连接

让Claude Code创建基础设施工件:

claude "创建 src/shared/redis.ts,使用ioredis建立Redis连接,支持环境变量配置,包含连接池和错误处理。同时创建 src/shared/types.ts 定义基础的Event和JobResult泛型类型。"

预期输出 — src/shared/redis.ts:

import Redis, { type RedisOptions } from 'ioredis';
import { logger } from './logger.js';

const getRedisConfig = (): RedisOptions => ({
  host: process.env.REDIS_HOST ?? 'localhost',
  port: parseInt(process.env.REDIS_PORT ?? '6379'),
  password: process.env.REDIS_PASSWORD,
  maxRetriesPerRequest: null, // BullMQ要求
  enableReadyCheck: true,
  retryStrategy: (times) => Math.min(times * 200, 5000),
});

// 创建连接池 - BullMQ需要专用连接
export const createRedis = (label: string): Redis => {
  const redis = new Redis(getRedisConfig());

  redis.on('error', (err) =>
    logger.error({ err, label }, 'Redis connection error')
  );
  redis.on('connect', () =>
    logger.info({ label }, 'Redis connected')
  );

  return redis;
};

预期输出 — src/shared/types.ts:

import { z } from 'zod';

// 基础事件类型
export interface BaseEvent<T = unknown> {
  id: string;
  type: string;
  timestamp: number;
  payload: T;
  correlationId: string;
}

// 任务结果
export type JobResult<T = unknown> =
  | { success: true; data: T }
  | { success: false; error: string; retryable: boolean };

// 事件处理器签名
export type EventHandler<T> = (
  event: BaseEvent<T>
) => Promise<JobResult>;

4.2 事件定义(Zod Schema验证)

claude "创建 src/events/schemas.ts,用Zod定义三种事件:
1. EmailEvent - 包含 to, subject, body, template?
2. ReportEvent - 包含 reportType(daily/weekly/monthly), dateRange, recipients[]
3. WebhookEvent - 包含 url, method, headers, body, retryCount
每个schema都要包含uuid和timestamp的自动生成。"

src/events/schemas.ts:

import { z } from 'zod';

const uuid = () => crypto.randomUUID();
const now = () => Date.now();

export const EmailEventSchema = z.object({
  id: z.string().uuid().default(uuid),
  type: z.literal('email.send'),
  timestamp: z.number().default(now),
  correlationId: z.string().uuid(),
  payload: z.object({
    to: z.string().email(),
    subject: z.string().min(1).max(200),
    body: z.string().min(1),
    template: z.enum(['welcome', 'notification', 'report']).optional(),
  }),
});

export const ReportEventSchema = z.object({
  id: z.string().uuid().default(uuid),
  type: z.literal('report.generate'),
  timestamp: z.number().default(now),
  correlationId: z.string().uuid(),
  payload: z.object({
    reportType: z.enum(['daily', 'weekly', 'monthly']),
    dateRange: z.object({
      start: z.string().datetime(),
      end: z.string().datetime(),
    }),
    recipients: z.array(z.string().email()).min(1),
  }),
});

export const WebhookEventSchema = z.object({
  id: z.string().uuid().default(uuid),
  type: z.literal('webhook.dispatch'),
  timestamp: z.number().default(now),
  correlationId: z.string().uuid(),
  payload: z.object({
    url: z.string().url(),
    method: z.enum(['POST', 'PUT', 'PATCH']),
    headers: z.record(z.string()).default({}),
    body: z.unknown(),
    retryCount: z.number().int().min(0).max(5).default(0),
  }),
});

// 类型推导
export type EmailEvent = z.infer<typeof EmailEventSchema>;
export type ReportEvent = z.infer<typeof ReportEventSchema>;
export type WebhookEvent = z.infer<typeof WebhookEventSchema>;

// 事件注册表 - 用于动态路由
export const EventRegistry = {
  'email.send': EmailEventSchema,
  'report.generate': ReportEventSchema,
  'webhook.dispatch': WebhookEventSchema,
} as const;

4.3 队列配置与生产者

claude "创建 src/queues/index.ts,定义三个BullMQ队列:email-queue、report-queue、webhook-queue。
配置合理的默认值:并发数3、尝试次数3、指数退避。
然后创建 src/producers/event-producer.ts,实现类型安全的事件发布接口。"

src/queues/index.ts:

import { Queue, type QueueOptions } from 'bullmq';
import { createRedis } from '../shared/redis.js';

const defaultQueueOptions: QueueOptions = {
  connection: createRedis('queue'),
  defaultJobOptions: {
    removeOnComplete: { count: 1000 },   // 保留最近1000条成功记录
    removeOnFail: { count: 5000 },        // 保留最近5000条失败记录
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 1000,  // 1s → 2s → 4s
    },
  },
};

export const emailQueue = new Queue('email-queue', {
  ...defaultQueueOptions,
  limiter: { max: 10, duration: 1000 }, // 每秒最多10封
});

export const reportQueue = new Queue('report-queue', {
  ...defaultQueueOptions,
  defaultJobOptions: {
    ...defaultQueueOptions.defaultJobOptions,
    attempts: 2, // 报表生成昂贵,少重试
  },
});

export const webhookQueue = new Queue('webhook-queue', {
  ...defaultQueueOptions,
  defaultJobOptions: {
    ...defaultQueueOptions.defaultJobOptions,
    backoff: {
      type: 'exponential',
      delay: 5000, // Webhook退避更保守
    },
  },
});

export const queues = { emailQueue, reportQueue, webhookQueue };

src/producers/event-producer.ts:

import { queues } from '../queues/index.js';
import { EventRegistry } from '../events/schemas.js';
import { logger } from '../shared/logger.js';
import type { JobResult } from '../shared/types.js';

type EventType = keyof typeof EventRegistry;

export class EventProducer {
  async publish<T extends EventType>(
    type: T,
    rawPayload: z.infer<typeof EventRegistry[T]>
  ): Promise<JobResult<{ jobId: string }>> {
    const schema = EventRegistry[type];

    // Zod验证 - 第一道防线
    const parsed = schema.safeParse(rawPayload);
    if (!parsed.success) {
      logger.warn({ type, errors: parsed.error.flatten() }, 'Event validation failed');
      return {
        success: false,
        error: `Validation: ${parsed.error.message}`,
        retryable: false,
      };
    }

    const event = parsed.data;
    const queueMap = {
      'email.send': queues.emailQueue,
      'report.generate': queues.reportQueue,
      'webhook.dispatch': queues.webhookQueue,
    };

    const queue = queueMap[type];
    const job = await queue.add(type, event, {
      jobId: event.id,
      // 事件元数据存入job,方便Dashboard追踪
      opts: { priority: this.getPriority(type) },
    });

    logger.info(
      { eventType: type, jobId: job.id, correlationId: event.correlationId },
      'Event published'
    );

    return { success: true, data: { jobId: job.id! } };
  }

  private getPriority(type: EventType): number {
    // 数字越小优先级越高
    const priorities: Record<EventType, number> = {
      'webhook.dispatch': 1,  // Webhook实时性要求最高
      'email.send': 5,
      'report.generate': 10,  // 报表可以稍慢
    };
    return priorities[type];
  }
}

export const producer = new EventProducer();

4.4 Worker实现

claude "创建 src/workers/email-worker.ts,实现邮件发送Worker。
要求:
1. 使用Worker类,并发度3
2. 模拟邮件发送(随机延迟+成功率95%)
3. 实现优雅关闭(SIGTERM/SIGINT)
4. 成功/失败都要写structured log
5. 失败时返回retryable信息"

src/workers/email-worker.ts:

import { Worker, type Job } from 'bullmq';
import { createRedis } from '../shared/redis.js';
import { logger } from '../shared/logger.js';
import { EmailEventSchema } from '../events/schemas.js';
import type { JobResult } from '../shared/types.js';

// 模拟邮件发送API
async function sendEmail(
  to: string,
  subject: string,
  body: string
): Promise<{ delivered: boolean; messageId: string }> {
  // 模拟网络延迟
  await new Promise((r) => setTimeout(r, 100 + Math.random() * 400));

  // 95%成功率
  if (Math.random() > 0.95) {
    throw new Error('SMTP connection refused');
  }

  return {
    delivered: true,
    messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
  };
}

const processor = async (job: Job): Promise<JobResult> => {
  const { data } = job;

  // 运行时验证
  const parsed = EmailEventSchema.safeParse(data);
  if (!parsed.success) {
    return {
      success: false,
      error: `Invalid event data: ${parsed.error.message}`,
      retryable: false,
    };
  }

  const event = parsed.data;
  logger.info(
    { jobId: job.id, to: event.payload.to, attempt: job.attemptsStarted },
    'Processing email job'
  );

  try {
    const result = await sendEmail(
      event.payload.to,
      event.payload.subject,
      event.payload.body
    );

    logger.info(
      { jobId: job.id, messageId: result.messageId },
      'Email sent successfully'
    );

    return { success: true, data: result };
  } catch (err) {
    const errorMsg = err instanceof Error ? err.message : 'Unknown error';
    logger.warn({ jobId: job.id, error: errorMsg }, 'Email send failed');

    return {
      success: false,
      error: errorMsg,
      retryable: true, // SMTP错误可重试
    };
  }
};

// 创建Worker实例
const worker = new Worker('email-queue', processor, {
  connection: createRedis('email-worker'),
  concurrency: 3,
  limiter: { max: 5, duration: 1000 },
});

// 进度更新
worker.on('progress', (job, progress) => {
  logger.debug({ jobId: job.id, progress }, 'Job progress');
});

// 完成事件
worker.on('completed', (job) => {
  logger.info({ jobId: job.id }, 'Job completed');
});

// 失败事件
worker.on('failed', (job, err) => {
  logger.error({ jobId: job?.id, error: err.message }, 'Job failed permanently');
});

// 优雅关闭
const shutdown = async (signal: string) => {
  logger.info({ signal }, 'Shutting down email worker...');
  await worker.close(true); // true = 等待当前任务完成
  process.exit(0);
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

logger.info('Email worker started');

4.5 事件总线——系统枢纽

claude "创建 src/events/event-bus.ts,实现一个EventBus类:
1. 基于Redis Pub/Sub的跨进程事件广播
2. 支持subscribe/unsubscribe/publish
3. 内置事件日志(写入Redis Stream做审计追踪)
4. 提供getEventHistory方法查询历史"

src/events/event-bus.ts:

import Redis from 'ioredis';
import { createRedis } from '../shared/redis.js';
import { logger } from '../shared/logger.js';
import type { BaseEvent, EventHandler } from '../shared/types.js';

type Listener<T = unknown> = {
  eventType: string;
  handler: EventHandler<T>;
};

export class EventBus {
  private publisher: Redis;
  private subscriber: Redis;
  private listeners: Map<string, Listener[]> = new Map();
  private streamKey = 'event-bus:history';

  constructor() {
    this.publisher = createRedis('event-bus-pub');
    this.subscriber = createRedis('event-bus-sub');

    this.subscriber.on('message', (channel, message) => {
      this.handleMessage(channel, message);
    });
  }

  async publish<T>(event: BaseEvent<T>): Promise<void> {
    const channel = `events:${event.type}`;
    const payload = JSON.stringify(event);

    // 广播给订阅者
    await this.publisher.publish(channel, payload);

    // 写入审计流
    await this.publisher.xadd(
      this.streamKey,
      '*',
      'id', event.id,
      'type', event.type,
      'payload', payload,
      'correlationId', event.correlationId
    );

    logger.info(
      { eventType: event.type, eventId: event.id, channel },
      'Event broadcasted'
    );
  }

  subscribe<T>(eventType: string, handler: EventHandler<T>): () => void {
    const channel = `events:${eventType}`;

    if (!this.listeners.has(channel)) {
      this.listeners.set(channel, []);
      this.subscriber.subscribe(channel);
    }

    const listener: Listener<T> = { eventType, handler };
    this.listeners.get(channel)!.push(listener as Listener);

    logger.info({ eventType, channel }, 'Subscribed to event');

    // 返回取消订阅函数
    return () => {
      const listeners = this.listeners.get(channel);
      if (listeners) {
        const idx = listeners.findIndex((l) => l.handler === handler);
        if (idx !== -1) listeners.splice(idx, 1);
        if (listeners.length === 0) {
          this.subscriber.unsubscribe(channel);
          this.listeners.delete(channel);
        }
      }
    };
  }

  private async handleMessage(channel: string, raw: string): Promise<void> {
    try {
      const event = JSON.parse(raw);
      const listeners = this.listeners.get(channel) ?? [];

      await Promise.allSettled(
        listeners.map(async ({ handler }) => {
          try {
            await handler(event);
          } catch (err) {
            logger.error(
              { err, eventType: event.type, eventId: event.id },
              'Event handler failed'
            );
          }
        })
      );
    } catch (err) {
      logger.error({ err, channel }, 'Failed to process event message');
    }
  }

  // 查询事件历史
  async getHistory(count = 50): Promise<BaseEvent[]> {
    const entries = await this.publisher.xrange(
      this.streamKey,
      '-',
      '+',
      'COUNT',
      count
    );

    return entries.map(([, fields]) => {
      const obj = Object.fromEntries(
        fields.reduce<string[]>((acc, val, i) => {
          if (i % 2 === 0) acc.push(val);
          else acc[acc.length - 1] = val;
          return acc;
        }, []).reduce<[string, string][]>((acc, _, i, arr) => {
          if (i % 2 === 0) acc.push([arr[i], arr[i + 1]]);
          return acc;
        }, [])
      );
      return JSON.parse(obj.payload);
    });
  }

  async disconnect(): Promise<void> {
    await this.subscriber.unsubscribe();
    await this.publisher.quit();
    await this.subscriber.quit();
  }
}

export const eventBus = new EventBus();

五、架构全景图

graph TB
    subgraph 客户端
        CLI[CLI/API调用]
        DASH[监控Dashboard]
    end

    subgraph 事件层
        EP[EventProducer<br/>Zod验证]
        EB[EventBus<br/>Redis Pub/Sub]
    end

    subgraph 队列层 - BullMQ
        EQ[email-queue]
        RQ[report-queue]
        WQ[webhook-queue]
    end

    subgraph Worker层
        EW[EmailWorker<br/>并发3]
        RW[ReportWorker<br/>并发2]
        WW[WebhookWorker<br/>并发5]
    end

    subgraph 存储
        REDIS[(Redis 7<br/>队列+Pub/Sub+Streams)]
    end

    CLI -->|发布事件| EP
    EP -->|验证+入队| EQ
    EP -->|验证+入队| RQ
    EP -->|验证+入队| WQ
    EP -->|广播| EB

    EB -->|审计日志| REDIS
    EQ -->|消费| EW
    RQ -->|消费| RW
    WQ -->|消费| WW

    EW -->|状态更新| REDIS
    RW -->|状态更新| REDIS
    WW -->|状态更新| REDIS

    DASH -->|读取状态| REDIS

    style EP fill:#6366f1,color:#fff
    style EB fill:#8b5cf6,color:#fff
    style REDIS fill:#dc2626,color:#fff
    style DASH fill:#059669,color:#fff

六、监控面板实现

6.1 REST API端点

claude "创建 src/monitoring/api.ts,使用Node.js原生http模块(不引入Express)创建监控API:
- GET /stats - 各队列的等待/活跃/完成/失败数
- GET /history - 最近事件历史
- GET /health - 健康检查(Redis连接状态)
端口从环境变量MONITOR_PORT读取,默认3001"

src/monitoring/api.ts:

import { createServer } from 'node:http';
import { queues } from '../queues/index.js';
import { createRedis } from '../shared/redis.js';
import { logger } from '../shared/logger.js';

const PORT = parseInt(process.env.MONITOR_PORT ?? '3001');

const healthRedis = createRedis('monitor');

async function getStats() {
  const [email, report, webhook] = await Promise.all([
    queues.emailQueue.getJobCounts(),
    queues.reportQueue.getJobCounts(),
    queues.webhookQueue.getJobCounts(),
  ]);

  return {
    timestamp: new Date().toISOString(),
    queues: {
      email: email,
      report: report,
      webhook: webhook,
    },
  };
}

async function getHealth() {
  try {
    await healthRedis.ping();
    return { status: 'healthy', redis: 'connected', uptime: process.uptime() };
  } catch {
    return { status: 'unhealthy', redis: 'disconnected' };
  }
}

const server = createServer(async (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Access-Control-Allow-Origin', '*');

  try {
    if (req.url === '/stats') {
      const stats = await getStats();
      res.writeHead(200);
      res.end(JSON.stringify(stats));
    } else if (req.url === '/health') {
      const health = await getHealth();
      res.writeHead(health.status === 'healthy' ? 200 : 503);
      res.end(JSON.stringify(health));
    } else {
      res.writeHead(404);
      res.end(JSON.stringify({ error: 'Not found' }));
    }
  } catch (err) {
    logger.error({ err }, 'Monitor API error');
    res.writeHead(500);
    res.end(JSON.stringify({ error: 'Internal error' }));
  }
});

server.listen(PORT, () => {
  logger.info({ port: PORT }, 'Monitor API started');
});

6.2 实时Dashboard页面

claude "创建 src/monitoring/dashboard.ts,用Node.js http模块提供一个单页面HTML dashboard。
页面功能:
1. 每2秒自动刷新队列状态(调用/stats API)
2. 用CSS Grid展示三个队列的状态卡片
3. 每个卡片显示:等待中、活跃、完成、失败的数字
4. 使用纯CSS动画,数字变化时有闪烁效果
5. 深色主题,适合运维大屏"

Claude Code会生成一个完整的HTML内联Dashboard。这里展示核心模板结构:

// src/monitoring/dashboard.ts 的关键HTML模板
const dashboardHTML = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Event-Driven Platform Dashboard</title>
  <style>
    * { margin: 0; box-sizing: border-box; }
    body { background: #0f172a; color: #e2e8f0; font-family: system-ui; padding: 2rem; }
    .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
    .card { background: #1e293b; border-radius: 12px; padding: 1.5rem;
            border: 1px solid #334155; transition: border-color 0.3s; }
    .card:hover { border-color: #6366f1; }
    .card h2 { font-size: 1.1rem; margin-bottom: 1rem; color: #94a3b8; }
    .metric { display: flex; justify-content: space-between; padding: 0.5rem 0;
              border-bottom: 1px solid #334155; }
    .metric:last-child { border: none; }
    .metric .value { font-weight: 700; font-size: 1.3rem; font-variant-numeric: tabular-nums; }
    .waiting .value { color: #facc15; }
    .active .value { color: #22d3ee; }
    .completed .value { color: #4ade80; }
    .failed .value { color: #f87171; }
    @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
    .flash { animation: pulse 0.5s ease; }
    .header { text-align: center; margin-bottom: 2rem; }
    .header h1 { font-size: 1.5rem; color: #f8fafc; }
    .header .ts { color: #64748b; font-size: 0.85rem; }
  </style>
</head>
<body>
  <div class="header">
    <h1>Event-Driven Platform Monitor</h1>
    <div class="ts" id="timestamp">--</div>
  </div>
  <div class="grid" id="queues"></div>
  <script>
    const API = location.port === '3002' ? 'http://localhost:3001' : '';
    async function refresh() {
      try {
        const res = await fetch(API + '/stats');
        const data = await res.json();
        document.getElementById('timestamp').textContent = data.timestamp;
        const container = document.getElementById('queues');
        const labels = { waiting: '等待中', active: '处理中',
                         completed: '已完成', failed: '已失败' };
        const cls = { waiting: 'waiting', active: 'active',
                      completed: 'completed', failed: 'failed' };
        container.innerHTML = Object.entries(data.queues).map(([name, counts]) => \`
          <div class="card">
            <h2>\${name.toUpperCase().replace('-', ' ')}</h2>
            \${Object.entries(labels).map(([key, label]) => \`
              <div class="metric \${cls[key]}">
                <span>\${label}</span>
                <span class="value flash">\${counts[key] ?? 0}</span>
              </div>
            \`).join('')}
          </div>
        \`).join('');
      } catch(e) { console.error('Fetch failed:', e); }
    }
    refresh();
    setInterval(refresh, 2000);
  </script>
</body>
</html>
`;

七、完整实战案例:异步通知平台

7.1 主入口编排

将所有模块串联为一个可运行的系统:

claude "创建 src/index.ts 作为系统入口,功能:
1. 加载dotenv配置
2. 启动所有Worker
3. 启动监控API和Dashboard
4. 提供一个交互式CLI,可以手动发送测试事件
5. 演示模式:启动后自动发送一批测试事件"

src/index.ts:

import 'dotenv/config';
import { producer } from './producers/event-producer.js';
import { eventBus } from './events/event-bus.js';
import { logger } from './shared/logger.js';
import './workers/email-worker.js';
import './monitoring/api.js';

async function demo() {
  logger.info('=== Event-Driven Platform Demo ===');

  // 订阅所有事件做审计日志
  const eventTypes = ['email.send', 'report.generate', 'webhook.dispatch'];
  const unsubscribers = eventTypes.map((type) =>
    eventBus.subscribe(type, async (event) => {
      logger.info(
        { eventType: type, eventId: event.id },
        '[Audit] Event received via bus'
      );
      return { success: true, data: 'logged' };
    })
  );

  // 发送一批测试事件
  const correlationId = crypto.randomUUID();

  // 1. 邮件事件
  const emailResult = await producer.publish('email.send', {
    id: crypto.randomUUID(),
    type: 'email.send',
    timestamp: Date.now(),
    correlationId,
    payload: {
      to: '[email protected]',
      subject: '欢迎使用事件驱动平台',
      body: '这是一封由BullMQ队列异步处理的邮件。',
      template: 'welcome',
    },
  });
  logger.info({ result: emailResult }, 'Email event published');

  // 2. 报表事件
  const reportResult = await producer.publish('report.generate', {
    id: crypto.randomUUID(),
    type: 'report.generate',
    timestamp: Date.now(),
    correlationId,
    payload: {
      reportType: 'daily',
      dateRange: {
        start: new Date(Date.now() - 86400000).toISOString(),
        end: new Date().toISOString(),
      },
      recipients: ['[email protected]'],
    },
  });
  logger.info({ result: reportResult }, 'Report event published');

  // 3. Webhook事件
  const webhookResult = await producer.publish('webhook.dispatch', {
    id: crypto.randomUUID(),
    type: 'webhook.dispatch',
    timestamp: Date.now(),
    correlationId,
    payload: {
      url: 'https://httpbin.org/post',
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { event: 'test', correlationId },
      retryCount: 0,
    },
  });
  logger.info({ result: webhookResult }, 'Webhook event published');

  logger.info('Demo events published. Workers are processing...');
  logger.info('Dashboard: http://localhost:3002');
  logger.info('API: http://localhost:3001/stats');
}

demo().catch((err) => {
  logger.fatal({ err }, 'Failed to start platform');
  process.exit(1);
});

7.2 启动与验证

# 终端1:启动Redis(如果还没启动)
docker start redis-eda

# 终端2:启动系统
npx tsx src/index.ts

预期输出:

[INFO] Redis connected (label="queue")
[INFO] Redis connected (label="email-worker")
[INFO] Redis connected (label="event-bus-pub")
[INFO] Redis connected (label="event-bus-sub")
[INFO] Email worker started
[INFO] Monitor API started (port=3001)
[INFO] === Event-Driven Platform Demo ===
[INFO] Event published (eventType="email.send", jobId="...")
[INFO] Event published (eventType="report.generate", jobId="...")
[INFO] Event published (eventType="webhook.dispatch", jobId="...")
[INFO] Processing email job (jobId="...", to="[email protected]")
[INFO] Email sent successfully (jobId="...", messageId="msg-...")
[INFO] Job completed (jobId="...")

7.3 验证监控面板

# 检查队列统计
curl http://localhost:3001/stats | jq .

# 输出:
# {
#   "timestamp": "2026-05-19T10:30:00.000Z",
#   "queues": {
#     "email": { "waiting": 0, "active": 0, "completed": 1, "failed": 0 },
#     "report": { "waiting": 0, "active": 1, "completed": 0, "failed": 0 },
#     "webhook": { "waiting": 0, "active": 0, "completed": 1, "failed": 0 }
#   }
# }

# 健康检查
curl http://localhost:3001/health | jq .
# { "status": "healthy", "redis": "connected", "uptime": 42.5 }

八、高级模式

8.1 死信队列(Dead Letter Queue)

当消息重试耗尽后,需要将其路由到死信队列做人工审查:

claude "在 src/workers/ 下创建 dead-letter-worker.ts,监听所有队列的failed事件,
将失败任务写入 Redis Sorted Set(按时间排序),
并提供查询接口。同时在 monitoring/api.ts 中添加 GET /dead-letters 端点。"

Claude Code会自动理解上下文并生成与现有架构一致的代码。核心模式如下:

// Worker失败时自动触发
worker.on('failed', async (job, err) => {
  if (job && job.attemptsMade >= job.opts?.attempts!) {
    // 写入死信队列
    await deadLetterRedis.zadd(
      'dead-letters',
      Date.now(),
      JSON.stringify({
        jobId: job.id,
        queueName: 'email-queue',
        data: job.data,
        error: err.message,
        failedAt: new Date().toISOString(),
      })
    );
    logger.error({ jobId: job.id }, 'Job moved to dead letter queue');
  }
});

8.2 延迟任务与定时调度

// 延迟发送 - 5分钟后
await emailQueue.add('email.send', eventPayload, {
  delay: 5 * 60 * 1000,
});

// 定时调度 - 使用BullMQ的Repeatable API
await reportQueue.add(
  'report.generate',
  { reportType: 'daily', /* ... */ },
  {
    repeat: { pattern: '0 6 * * *' }, // 每天早上6点
    jobId: 'daily-report',
  }
);

8.3 优先级队列与速率限制

graph TD
    IN[事件入口] --> P{优先级判断}
    P -->|urgent| H[高优先级队列<br/>priority=1]
    P -->|normal| M[普通队列<br/>priority=5]
    P -->|batch| L[批量队列<br/>priority=10]
    H --> W[Worker<br/>concurrency=5]
    M --> W
    L --> W
    W -->|超限| RL[速率限制器<br/>10 req/s]
    RL --> EXT[外部API]

    style H fill:#ef4444,color:#fff
    style M fill:#f59e0b,color:#fff
    style L fill:#6b7280,color:#fff
    style RL fill:#8b5cf6,color:#fff
// 在Producer中动态设置优先级
async publishWithPriority(
  type: EventType,
  payload: unknown,
  priority: 'urgent' | 'normal' | 'batch'
) {
  const priorityMap = { urgent: 1, normal: 5, batch: 10 };
  return queue.add(type, payload, {
    priority: priorityMap[priority],
  });
}

九、测试策略

9.1 使用Vitest进行单元测试

claude "为 event-producer.ts 编写Vitest单元测试。
要求:
1. Mock BullMQ的Queue类
2. 测试正常发布流程
3. 测试Zod验证失败的场景
4. 测试优先级分配逻辑
5. 使用beforeEach/afterEach管理Mock"

src/producers/__tests__/event-producer.test.ts:

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { producer } from '../event-producer.js';

// Mock BullMQ
vi.mock('../../queues/index.js', () => ({
  queues: {
    emailQueue: { add: vi.fn() },
    reportQueue: { add: vi.fn() },
    webhookQueue: { add: vi.fn() },
  },
}));

import { queues } from '../../queues/index.js';

describe('EventProducer', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should publish valid email event', async () => {
    const mockAdd = vi.mocked(queues.emailQueue.add).mockResolvedValue({
      id: 'job-123',
    } as any);

    const result = await producer.publish('email.send', {
      id: crypto.randomUUID(),
      type: 'email.send',
      timestamp: Date.now(),
      correlationId: crypto.randomUUID(),
      payload: {
        to: '[email protected]',
        subject: 'Test',
        body: 'Test body',
      },
    });

    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.jobId).toBe('job-123');
    }
    expect(mockAdd).toHaveBeenCalledOnce();
  });

  it('should reject invalid email', async () => {
    const result = await producer.publish('email.send', {
      id: crypto.randomUUID(),
      type: 'email.send',
      timestamp: Date.now(),
      correlationId: crypto.randomUUID(),
      payload: {
        to: 'not-an-email',
        subject: 'Test',
        body: 'Test body',
      },
    });

    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.retryable).toBe(false);
      expect(result.error).toContain('Validation');
    }
  });
});

9.2 运行测试

npx vitest run
#  ✓ event-producer.test.ts (2 tests) 15ms
#  Tests  2 passed (2)
#  Duration  320ms

十、生产部署清单

10.1 Docker Compose部署

# docker-compose.yml
version: '3.8'

services:
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 5s
      retries: 5

  email-worker:
    build: .
    command: npx tsx src/workers/email-worker.ts
    depends_on:
      redis: { condition: service_healthy }
    environment:
      REDIS_HOST: redis
    deploy:
      replicas: 2  # 水平扩展
      resources:
        limits: { memory: 256M }

  report-worker:
    build: .
    command: npx tsx src/workers/report-worker.ts
    depends_on:
      redis: { condition: service_healthy }
    environment:
      REDIS_HOST: redis

  monitor:
    build: .
    command: npx tsx src/monitoring/api.ts
    depends_on:
      redis: { condition: service_healthy }
    ports:
      - '3001:3001'
    environment:
      REDIS_HOST: redis

volumes:
  redis-data:

10.2 关键配置参数

参数 开发环境 生产环境 说明
Worker并发数 3 10-50 根据下游API承受能力调整
最大重试次数 3 3-5 根据任务重要性
退避策略 exponential 1s exponential 5s 生产环境更保守
Redis内存 无限制 512MB+ 设定maxmemory防OOM
连接池 5 20-50 每个Worker独立连接
任务TTL 默认 24h 防止僵尸任务堆积

10.3 健康检查与告警

// 添加到 monitoring/api.ts 的告警逻辑
async function checkAlerts(): Promise<string[]> {
  const alerts: string[] = [];
  const emailCounts = await queues.emailQueue.getJobCounts();

  if (emailCounts.waiting > 1000) {
    alerts.push('email-queue堆积超过1000,需要扩容Worker');
  }
  if (emailCounts.failed > 100) {
    alerts.push('email-queue失败数超过100,检查SMTP服务');
  }

  const deadLetterCount = await deadLetterRedis.zcard('dead-letters');
  if (deadLetterCount > 50) {
    alerts.push(`死信队列中有${deadLetterCount}条消息待处理`);
  }

  return alerts;
}

十一、事件流转完整时序图

sequenceDiagram
    participant C as 客户端
    participant P as Producer
    participant Z as Zod验证
    participant Q as BullMQ Queue
    participant W as Worker
    participant BUS as EventBus
    participant R as Redis
    participant M as Monitor

    C->>P: publish('email.send', payload)
    P->>Z: safeParse(payload)

    alt 验证失败
        Z-->>P: ❌ Validation Error
        P-->>C: {success: false, retryable: false}
    else 验证通过
        Z-->>P: ✅ Parsed Event
        P->>Q: queue.add(type, event)
        Q->>R: LPUSH job data
        P->>BUS: broadcast(event)
        BUS->>R: XADD audit stream
        BUS->>R: PUBLISH events:email.send
        P-->>C: {success: true, jobId: '...'}
    end

    Note over Q,W: 异步处理
    Q->>W: BRPOP job
    W->>W: process(job)
    W->>R: 更新job状态(active)
    W->>W: 执行业务逻辑
    W->>R: 更新job状态(completed)

    M->>R: GET job counts
    R-->>M: stats data
    M-->>C: Dashboard展示

十二、常见问题与解决方案

问题 症状 解决方案
Worker启动后不消费 日志显示connected但无处理记录 检查Redis maxRetriesPerRequest: null 配置
任务堆积不处理 waiting数持续增长 增加Worker并发数或replicas
内存持续增长 Redis内存使用量线性上升 配置removeOnComplete和removeOnFail
重复消费 同一任务被两个Worker执行 确认Worker使用不同的connection实例
优雅关闭失败 docker stop超时 检查worker.close(true)的timeout设置

延伸阅读


训练营适用性评估

维度 评估
适合训练营类型 后端架构训练营、Node.js进阶营、分布式系统实战营
教学时长 6-8小时(含动手实验)
难度等级 ⭐⭐⭐⭐ (中高级)
实验环境要求 每人需Docker + Node.js 20 + Redis
最佳分组 2-3人一组,每组实现一个Worker
产出物 完整可运行的事件驱动平台 + Dashboard

参考来源

  1. Designing Event-Driven Architecture with Claude Code — 事件驱动架构设计模式与Claude Code实践指南
  2. Claude Code Job Queues and Async Processing — BullMQ集成与异步任务系统实现教程
  3. BullMQ Job Queues in Node.js: Background Processing Done Right — BullMQ生产级最佳实践与配置参数详解
  4. Claude Event-Driven Agents (GitHub) — 基于Redis消息队列的事件驱动Agent架构参考实现
  5. Scaling WebSockets with Claude Code: Redis Pub/Sub — Redis Pub/Sub与WebSocket横向扩展实战

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入