如何使用CQRS进行事件驱动的任务开发

148 阅读17分钟

在使用 Go 的 Watermill 库进行 CQRS 和事件驱动开发时,异步调用第三方接口并处理结果是一个常见的场景。在事件创建后,通过一个事件来触发异步调用,并通过某种机制(如 for 循环)监听调用结果,同时处理超时逻辑。这种方式在实际开发中可能不是最优的,因为 Watermill 的事件驱动模型更倾向于通过发布/订阅(Pub/Sub)机制来解耦事件的生产和消费,而不建议在事件处理中直接使用 for 循环来轮询结果。

以下是一个更符合 Watermill 设计理念的实现方式:

  1. 事件触发:当某个业务事件发生时,发布一个事件(例如 CallThirdPartyAPI)。
  2. 事件处理器:订阅该事件,在处理器中异步调用第三方接口。
  3. 结果处理:调用完成后,发布一个新的事件(如 ThirdPartyAPIResult)来表示结果或超时状态。
  4. 超时控制:使用 Go 的 context 包来管理超时逻辑。
  5. 解耦:避免在事件处理器中使用 for 循环轮询,而是利用 Watermill 的 Pub/Sub 机制来处理异步结果。

下面是一个完整的示例,展示如何使用 Watermill 实现异步调用第三方接口,并处理结果和超时逻辑。

示例代码

假设我们有一个场景:当用户注册(UserRegistered 事件)时,需要调用第三方接口发送欢迎邮件,并处理调用结果或超时情况。

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/ThreeDotsLabs/watermill"
	"github.com/ThreeDotsLabs/watermill/message"
	"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
	"golang.org/x/sync/errgroup"
)

// UserRegistered 事件,表示用户注册
type UserRegistered struct {
	UserID    string
	Email     string
	Timestamp time.Time
}

// CallThirdPartyAPI 事件,表示需要调用第三方接口
type CallThirdPartyAPI struct {
	UserID     string
	Email      string
	AttemptID  string
	Timestamp  time.Time
}

// ThirdPartyAPIResult 事件,表示第三方接口调用结果
type ThirdPartyAPIResult struct {
	UserID     string
	AttemptID  string
	Success    bool
	Error      string
	Timestamp  time.Time
}

// 模拟第三方接口调用
func callThirdPartyAPI(email string) (bool, error) {
	// 模拟网络延迟
	time.Sleep(500 * time.Millisecond)
	if email == "fail@example.com" {
		return false, fmt.Errorf("failed to send email to %s", email)
	}
	return true, nil
}

func main() {
	// 创建 Watermill 的日志
	logger := watermill.NewStdLogger(false, false)

	// 使用 GoChannel 作为 Pub/Sub(生产环境可替换为 Kafka、RabbitMQ 等)
	pubSub := gochannel.NewGoChannel(
		gochannel.Config{},
		logger,
	)

	// 发布 UserRegistered 事件
	publisher := pubSub

	// 订阅 CallThirdPartyAPI 事件,处理异步调用
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "call_third_party_api")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var event CallThirdPartyAPI
			if err := json.Unmarshal(msg.Payload, &event); err != nil {
				log.Printf("Failed to unmarshal event: %v", err)
				msg.Ack()
				continue
			}

			// 使用 context 控制超时
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()

			// 异步调用第三方接口
			resultCh := make(chan ThirdPartyAPIResult, 1)
			go func() {
				success, err := callThirdPartyAPI(event.Email)
				result := ThirdPartyAPIResult{
					UserID:    event.UserID,
					AttemptID: event.AttemptID,
					Success:   success,
					Timestamp: time.Now(),
				}
				if err != nil {
					result.Error = err.Error()
				}
				resultCh <- result
			}()

			// 等待结果或超时
			select {
			case result := <-resultCh:
				// 发布结果事件
				payload, err := json.Marshal(result)
				if err != nil {
					log.Printf("Failed to marshal result: %v", err)
					msg.Ack()
					continue
				}
				resultMsg := message.NewMessage(watermill.NewUUID(), payload)
				if err := publisher.Publish("third_party_api_result", resultMsg); err != nil {
					log.Printf("Failed to publish result: %v", err)
				}
			case <-ctx.Done():
				// 超时处理
				result := ThirdPartyAPIResult{
					UserID:    event.UserID,
					AttemptID: event.AttemptID,
					Success:   false,
					Error:     "timeout calling third-party API",
					Timestamp: time.Now(),
				}
				payload, err := json.Marshal(result)
				if err != nil {
					log.Printf("Failed to marshal timeout result: %v", err)
					msg.Ack()
					continue
				}
				resultMsg := message.NewMessage(watermill.NewUUID(), payload)
				if err := publisher.Publish("third_party_api_result", resultMsg); err != nil {
					log.Printf("Failed to publish timeout result: %v", err)
				}
			}
			msg.Ack()
		}
	}()

	// 订阅 ThirdPartyAPIResult 事件,处理调用结果
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "third_party_api_result")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var result ThirdPartyAPIResult
			if err := json.Unmarshal(msg.Payload, &result); err != nil {
				log.Printf("Failed to unmarshal result: %v", err)
				msg.Ack()
				continue
			}
			if result.Success {
				log.Printf("Successfully sent email to user %s (attempt %s)", result.UserID, result.AttemptID)
			} else {
				log.Printf("Failed to send email to user %s (attempt %s): %s", result.UserID, result.AttemptID, result.Error)
			}
			msg.Ack()
		}
	}()

	// 模拟用户注册,触发 UserRegistered 事件
	userRegistered := UserRegistered{
		UserID:    "user123",
		Email:     "test@example.com",
		Timestamp: time.Now(),
	}
	payload, err := json.Marshal(userRegistered)
	if err != nil {
		log.Fatalf("Failed to marshal event: %v", err)
	}

	// 发布 UserRegistered 事件
	userMsg := message.NewMessage(watermill.NewUUID(), payload)
	if err := publisher.Publish("user_registered", userMsg); err != nil {
		log.Fatalf("Failed to publish: %v", err)
	}

	// 订阅 UserRegistered 事件,触发 CallThirdPartyAPI
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "user_registered")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var event UserRegistered
			if err := json.Unmarshal(msg.Payload, &event); err != nil {
				log.Printf("Failed to unmarshal event: %v", err)
				msg.Ack()
				continue
			}

			// 触发 CallThirdPartyAPI 事件
			apiCall := CallThirdPartyAPI{
				UserID:    event.UserID,
				Email:     event.Email,
				AttemptID: watermill.NewUUID(),
				Timestamp: time.Now(),
			}
			payload, err := json.Marshal(apiCall)
			if err != nil {
				log.Printf("Failed to marshal api call event: %v", err)
				msg.Ack()
				continue
			}
			apiMsg := message.NewMessage(watermill.NewUUID(), payload)
			if err := publisher.Publish("call_third_party_api", apiMsg); err != nil {
				log.Printf("Failed to publish api call: %v", err)
			}
			msg.Ack()
		}
	}()

	// 保持程序运行
	select {}
}

代码说明

  1. 事件定义

    • UserRegistered:表示用户注册的事件,触发后续逻辑。
    • CallThirdPartyAPI:表示需要调用第三方接口的事件,包含用户 ID 和邮件地址等信息。
    • ThirdPartyAPIResult:表示第三方接口调用的结果,包含成功/失败状态和错误信息。
  2. Watermill Pub/Sub

    • 使用 gochannel.NewGoChannel 作为消息通道(生产环境中可替换为 Kafka 或 RabbitMQ)。
    • 发布和订阅事件通过 publisher.PublishpubSub.Subscribe 实现。
  3. 异步调用与超时

    • 在处理 CallThirdPartyAPI 事件时,使用 Go 协程异步调用第三方接口(callThirdPartyAPI)。
    • 使用 context.WithTimeout 设置 2 秒超时。
    • 通过 select 监听调用结果或超时,超时后发布包含错误信息的 ThirdPartyAPIResult 事件。
  4. 结果处理

    • 订阅 third_party_api_result 事件,处理调用结果(成功或失败)。
    • 使用 log.Printf 输出结果,实际场景中可以更新数据库或执行其他操作。
  5. 避免 for 循环轮询

    • 没有使用 for 循环来轮询结果,而是通过事件驱动的方式,利用 Watermill 的 Pub/Sub 机制来传递和处理结果。
    • 这种方式更符合 CQRS 和事件驱动架构的解耦理念。

运行说明

  1. 依赖安装

    • 确保已安装 Watermill 库:
      go get github.com/ThreeDotsLabs/watermill
      go get github.com/ThreeDotsLabs/watermill-amqp
      
    • 本例使用 gochannel 作为 Pub/Sub 后端,无需额外依赖。生产环境中可替换为 Kafka 或 RabbitMQ。
  2. 运行程序

    • 运行 go run main.go,程序会模拟用户注册,触发事件链:
      • UserRegisteredCallThirdPartyAPIThirdPartyAPIResult
    • 日志会显示邮件发送的成功或失败(包括超时)。
  3. 输出示例

    • 成功案例:
      Successfully sent email to user user123 (attempt <uuid>)
      
    • 失败案例(模拟失败邮件):
      Failed to send email to user user123 (attempt <uuid>): failed to send email to fail@example.com
      
    • 超时案例(如果第三方接口延迟超过 2 秒):
      Failed to send email to user user123 (attempt <uuid>): timeout calling third-party API
      

为什么不使用 for 循环监听?

在事件中启动一个 for 循环来监听调用结果和超时。这种方式有以下问题:

  • 阻塞事件处理器:for 循环可能导致事件处理器阻塞,降低系统吞吐量。
  • 复杂性增加:轮询逻辑会使代码复杂,难以维护。
  • 资源浪费:轮询会占用 CPU 资源,尤其在高并发场景下。

相反,使用 contextselect 结合异步协程可以:

  • 优雅地处理超时。
  • 保持事件处理器的非阻塞性。
  • 通过 Watermill 的事件机制解耦调用和结果处理。

扩展建议

  1. 重试机制

    • 如果第三方接口调用失败,可以发布一个 RetryThirdPartyAPI 事件,包含重试次数,配合指数退避策略。
  2. 持久化

    • 将事件存储到数据库(如 Event Sourcing),以便追踪和审计。
  3. 生产环境

    • 替换 gochannel 为 Kafka 或 RabbitMQ(Watermill 支持多种后端)。
    • 使用 Watermill 的 CQRS 组件(如 github.com/ThreeDotsLabs/watermill/components/cqrs)来进一步简化命令和事件处理。
  4. 错误处理

    • 在生产环境中,添加监控和告警机制,记录失败的调用。

参考资料

这个实现展示了如何在 Watermill 中结合 CQRS 和事件驱动架构处理异步第三方接口调用,同时保持代码的清晰和可扩展性。如果有其他具体需求(例如重试逻辑或特定第三方接口),可以进一步定制! 当第三方接口是异步的(即接口调用快速返回,但实际任务的执行结果需要通过轮询或回调机制获取),在 Watermill 的 CQRS 和事件驱动架构中,我们需要一种方式来跟踪任务的最终状态,而不依赖 for 循环轮询以保持事件处理器的非阻塞性和解耦性。以下是解决这个问题的推荐方法:

解决方案概述

  1. 触发异步调用:在处理某个事件(如 CallThirdPartyAPI)时,调用第三方异步接口,获取一个任务 ID。
  2. 发布状态查询事件:接口返回任务 ID 后,发布一个新事件(如 CheckTaskStatus),包含任务 ID 和相关元数据。
  3. 异步状态检查:订阅 CheckTaskStatus 事件,定期检查任务状态(通过第三方接口提供的状态查询接口),并在必要时重新发布 CheckTaskStatus 事件以实现延迟重试。
  4. 结果处理:当任务状态表明完成(成功或失败)时,发布 ThirdPartyAPIResult 事件,供后续处理器消费。
  5. 超时控制:使用 context 或事件元数据跟踪超时,避免无限检查。
  6. 避免 for 循环:通过事件驱动的延迟机制(例如定时重新发布事件),代替阻塞的 for 循环轮询。

这种方式利用 Watermill 的事件发布/订阅机制,通过事件循环代替直接的 for 循环,保持系统的解耦和可扩展性。

示例场景

假设我们有一个异步第三方接口(如发送邮件的 API),调用后返回一个任务 ID,需要通过另一个查询接口检查任务状态。以下是一个完整的 Go 示例,展示如何在 Watermill 中处理这种情况。

示例代码

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/ThreeDotsLabs/watermill"
	"github.com/ThreeDotsLabs/watermill/message"
	"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
)

// UserRegistered 事件,表示用户注册
type UserRegistered struct {
	UserID    string
	Email     string
	Timestamp time.Time
}

// CallThirdPartyAPI 事件,表示需要调用第三方异步接口
type CallThirdPartyAPI struct {
	UserID     string
	Email      string
	AttemptID  string
	Timestamp  time.Time
}

// CheckTaskStatus 事件,表示需要检查任务状态
type CheckTaskStatus struct {
	UserID     string
	AttemptID  string
	TaskID     string
	RetryCount int
	MaxRetries int
	Timestamp  time.Time
}

// ThirdPartyAPIResult 事件,表示第三方接口调用结果
type ThirdPartyAPIResult struct {
	UserID     string
	AttemptID  string
	TaskID     string
	Success    bool
	Error      string
	Timestamp  time.Time
}

// 模拟第三方异步接口调用
func callThirdPartyAsyncAPI(email string) (string, error) {
	// 模拟快速返回任务 ID
	return watermill.NewUUID(), nil
}

// 模拟查询任务状态
func checkTaskStatus(taskID string) (string, error) {
	// 模拟任务状态:pending, completed, failed
	// 假设前几次调用返回 "pending",第 3 次返回 "completed"
	// 这里用 taskID 的哈希模拟状态变化,实际场景应调用第三方 API
	hash := int(taskID[0]) // 简单模拟
	if hash%2 == 0 {
            return "pending", nil
	}
	if hash%3 == 0 {
		return "completed", nil
	}
	return "failed", fmt.Errorf("task %s failed", taskID)
}

func main() {
	// 创建 Watermill 日志
	logger := watermill.NewStdLogger(false, false)

	// 使用 GoChannel 作为 Pub/Sub(生产环境可替换为 Kafka、RabbitMQ)
	pubSub := gochannel.NewGoChannel(
		gochannel.Config{},
		logger,
	)

	// 发布者
	publisher := pubSub

	// 订阅 CallThirdPartyAPI 事件,调用异步接口
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "call_third_party_api")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var event CallThirdPartyAPI
			if err := json.Unmarshal(msg.Payload, &event); err != nil {
				log.Printf("Failed to unmarshal event: %v", err)
				msg.Ack()
				continue
			}

			// 调用第三方异步接口
			taskID, err := callThirdPartyAsyncAPI(event.Email)
			if err != nil {
				// 如果调用失败,直接发布失败结果
				result := ThirdPartyAPIResult{
					UserID:    event.UserID,
					AttemptID: event.AttemptID,
					TaskID:    "",
					Success:   false,
					Error:     fmt.Sprintf("Failed to call API: %v", err),
					Timestamp: time.Now(),
				}
				payload, err := json.Marshal(result)
				if err != nil {
					log.Printf("Failed to marshal result: %v", err)
					msg.Ack()
					continue
				}
				resultMsg := message.NewMessage(watermill.NewUUID(), payload)
				if err := publisher.Publish("third_party_api_result", resultMsg); err != nil {
					log.Printf("Failed to publish result: %v", err)
				}
				msg.Ack()
				continue
			}

			// 发布 CheckTaskStatus 事件
			checkEvent := CheckTaskStatus{
				UserID:     event.UserID,
				AttemptID:  event.AttemptID,
				TaskID:     taskID,
				RetryCount: 0,
				MaxRetries: 5, // 最大重试 5 次
				Timestamp:  time.Now(),
			}
			payload, err := json.Marshal(checkEvent)
			if err != nil {
				log.Printf("Failed to marshal check event: %v", err)
				msg.Ack()
				continue
			}
			checkMsg := message.NewMessage(watermill.NewUUID(), payload)
			if err := publisher.Publish("check_task_status", checkMsg); err != nil {
				log.Printf("Failed to publish check event: %v", err)
			}
			msg.Ack()
		}
	}()

	// 订阅 CheckTaskStatus 事件,检查任务状态
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "check_task_status")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var event CheckTaskStatus
			if err := json.Unmarshal(msg.Payload, &event); err != nil {
				log.Printf("Failed to unmarshal check event: %v", err)
				msg.Ack()
				continue
			}

			// 检查任务状态
			status, err := checkTaskStatus(event.TaskID)
			if err != nil {
				// 状态检查失败,发布失败结果
				result := ThirdPartyAPIResult{
					UserID:    event.UserID,
					AttemptID: event.AttemptID,
					TaskID:    event.TaskID,
					Success:   false,
					Error:     fmt.Sprintf("Failed to check status: %v", err),
					Timestamp: time.Now(),
				}
				payload, err := json.Marshal(result)
				if err != nil {
					log.Printf("Failed to marshal result: %v", err)
					msg.Ack()
					continue
				}
				resultMsg := message.NewMessage(watermill.NewUUID(), payload)
				if err := publisher.Publish("third_party_api_result", resultMsg); err != nil {
					log.Printf("Failed to publish result: %v", err)
				}
				msg.Ack()
				continue
			}

			if status == "completed" {
				// 任务完成,发布成功结果
				result := ThirdPartyAPIResult{
					UserID:    event.UserID,
					AttemptID: event.AttemptID,
					TaskID:    event.TaskID,
					Success:   true,
					Timestamp: time.Now(),
				}
				payload, err := json.Marshal(result)
				if err != nil {
					log.Printf("Failed to marshal result: %v", err)
					msg.Ack()
					continue
				}
				resultMsg := message.NewMessage(watermill.NewUUID(), payload)
				if err := publisher.Publish("third_party_api_result", resultMsg); err != nil {
					log.Printf("Failed to publish result: %v", err)
				}
				msg.Ack()
				continue
			}

			// 如果任务仍在 pending 且未达到最大重试次数,延迟后重新发布 CheckTaskStatus
			if status == "pending" && event.RetryCount < event.MaxRetries {
				// 延迟 1 秒后重新发布(模拟指数退避)
				time.Sleep(1 * time.Second)
				event.RetryCount++
				event.Timestamp = time.Now()
				payload, err := json.Marshal(event)
				if err != nil {
					log.Printf("Failed to marshal retry event: %v", err)
					msg.Ack()
					continue
				}
				retryMsg := message.NewMessage(watermill.NewUUID(), payload)
				if err := publisher.Publish("check_task_status", retryMsg); err != nil {
					log.Printf("Failed to publish retry event: %v", err)
				}
				msg.Ack()
				continue
			}

			// 达到最大重试次数或状态未知,发布失败结果
			result := ThirdPartyAPIResult{
				UserID:    event.UserID,
				AttemptID: event.AttemptID,
				TaskID:    event.TaskID,
				Success:   false,
				Error:     fmt.Sprintf("Task %s did not complete after %d retries, last status: %s", event.TaskID, event.MaxRetries, status),
				Timestamp: time.Now(),
			}
			payload, err := json.Marshal(result)
			if err != nil {
				log.Printf("Failed to marshal result: %v", err)
				msg.Ack()
				continue
			}
			resultMsg := message.NewMessage(watermill.NewUUID(), payload)
			if err := publisher.Publish("third_party_api_result", resultMsg); err != nil {
				log.Printf("Failed to publish result: %v", err)
			}
			msg.Ack()
		}
	}()

	// 订阅 ThirdPartyAPIResult 事件,处理最终结果
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "third_party_api_result")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var result ThirdPartyAPIResult
			if err := json.Unmarshal(msg.Payload, &result); err != nil {
				log.Printf("Failed to unmarshal result: %v", err)
				msg.Ack()
				continue
			}
			if result.Success {
				log.Printf("Task %s for user %s (attempt %s) completed successfully", result.TaskID, result.UserID, result.AttemptID)
			} else {
				log.Printf("Task %s for user %s (attempt %s) failed: %s", result.TaskID, result.UserID, result.AttemptID, result.Error)
			}
			msg.Ack()
		}
	}()

	// 模拟用户注册,触发 UserRegistered 事件
	userRegistered := UserRegistered{
		UserID:    "user123",
		Email:     "test@example.com",
		Timestamp: time.Now(),
	}
	payload, err := json.Marshal(userRegistered)
	if err != nil {
		log.Fatalf("Failed to marshal event: %v", err)
	}

	// 发布 UserRegistered 事件
	userMsg := message.NewMessage(watermill.NewUUID(), payload)
	if err := publisher.Publish("user_registered", userMsg); err != nil {
		log.Fatalf("Failed to publish: %v", err)
	}

	// 订阅 UserRegistered 事件,触发 CallThirdPartyAPI
	go func() {
		messages, err := pubSub.Subscribe(context.Background(), "user_registered")
		if err != nil {
			log.Fatalf("Failed to subscribe: %v", err)
		}
		for msg := range messages {
			var event UserRegistered
			if err := json.Unmarshal(msg.Payload, &event); err != nil {
				log.Printf("Failed to unmarshal event: %v", err)
				msg.Ack()
				continue
			}

			// 触发 CallThirdPartyAPI 事件
			apiCall := CallThirdPartyAPI{
				UserID:    event.UserID,
				Email:     event.Email,
				AttemptID: watermill.NewUUID(),
				Timestamp: time.Now(),
			}
			payload, err := json.Marshal(apiCall)
			if err != nil {
				log.Printf("Failed to marshal api call event: %v", err)
				msg.Ack()
				continue
			}
			apiMsg := message.NewMessage(watermill.NewUUID(), payload)
			if err := publisher.Publish("call_third_party_api", apiMsg); err != nil {
				log.Printf("Failed to publish api call: %v", err)
			}
			msg.Ack()
		}
	}()

	// 保持程序运行
	select {}
}

代码说明

  1. 事件定义

    • UserRegistered:用户注册事件,触发异步调用流程。
    • CallThirdPartyAPI:触发异步接口调用的事件。
    • CheckTaskStatus:检查任务状态的事件,包含任务 ID、重试次数和最大重试次数。
    • ThirdPartyAPIResult:最终结果事件,记录任务的成功或失败。
  2. 异步接口调用

    • callThirdPartyAsyncAPI 模拟第三方异步接口,返回任务 ID。
    • 处理器在收到 CallThirdPartyAPI 事件后调用接口,并发布 CheckTaskStatus 事件。
  3. 状态检查

    • checkTaskStatus 模拟查询任务状态,返回 "pending"、"completed" 或 "failed"。

    • 在 CheckTaskStatus 处理器中:

      • 如果状态是 "completed",发布成功的结果事件。
      • 如果状态是 "failed",发布失败的结果事件。
      • 如果状态是 "pending" 且未达到最大重试次数,延迟 1 秒后重新发布 CheckTaskStatus 事件。
      • 如果达到最大重试次数,发布失败的结果事件。
  4. 超时和重试

    • 通过 RetryCount 和 MaxRetries 控制重试次数,避免无限检查。
    • 使用 time.Sleep 模拟延迟,生产环境中可以使用更精确的定时器(如 time.After)或消息队列的延迟功能(如 Kafka 的延迟主题)。
  5. 避免 for 循环

    • 通过事件驱动的方式,CheckTaskStatus 事件在每次检查后决定是否重新发布自身,形成一个“事件循环”,代替阻塞的 for 循环。
    • 这种方式保持了事件处理器的非阻塞性,且符合 Watermill 的解耦设计。
  6. 结果处理

    • ThirdPartyAPIResult 事件的处理器记录任务的最终状态(成功或失败)。
    • 实际场景中,可以将结果存储到数据库或触发其他业务逻辑。

运行说明

  1. 依赖安装

    bash

    复制

    go get github.com/ThreeDotsLabs/watermill

    本例使用 gochannel 作为 Pub/Sub 后端,生产环境中可替换为 Kafka 或 RabbitMQ。

  2. 运行程序

    • 运行 go run main.go,程序会模拟用户注册,触发事件链:

      • UserRegistered → CallThirdPartyAPI → CheckTaskStatus → ThirdPartyAPIResult。
    • 日志会显示任务状态的检查和最终结果。

  3. 输出示例

    • 成功案例:

      text

      复制

      Task <task_id> for user user123 (attempt <uuid>) completed successfully

    • 失败案例:

      text

      复制

      Task <task_id> for user user123 (attempt <uuid>) failed: task <task_id> failed

    • 超时(达到最大重试次数):

      text

      复制

      Task <task_id> for user user123 (attempt <uuid>) failed: Task <task_id> did not complete after 5 retries, last status: pending

为什么不用 for 循环?

直接在事件处理器中使用 for 循环轮询任务状态会导致:

  • 阻塞:处理器会被阻塞,降低系统吞吐量。
  • 耦合:轮询逻辑与事件处理耦合,不利于扩展和维护。
  • 资源浪费:持续轮询会占用 CPU 资源,尤其在高并发场景下。

通过事件驱动的 CheckTaskStatus 事件循环,我们可以:

  • 非阻塞:每次状态检查都在新的消息处理中完成,处理器保持高效。
  • 解耦:状态检查逻辑与结果处理逻辑分离,符合 CQRS 原则。
  • 灵活性:可以通过调整 MaxRetries 或延迟时间来控制检查频率。

生产环境优化

  1. 延迟机制

    • 使用消息队列的延迟功能(如 RabbitMQ 的 TTL 或 Kafka 的延迟主题)代替 time.Sleep。
    • 例如,Watermill 支持 RabbitMQ,可以设置消息的 TTL。
  2. 指数退避

    • 在 CheckTaskStatus 处理器中,增加指数退避策略,减少对第三方接口的频繁调用。
    • 示例:延迟时间可以是 baseDelay * (2 ^ RetryCount)。
  3. 持久化

    • 将 CheckTaskStatus 和 ThirdPartyAPIResult 事件存储到数据库(如 Event Sourcing),以便追踪任务状态和审计。
  4. 监控和告警

    • 记录每次状态检查的日志,集成监控系统(如 Prometheus)以跟踪失败率和延迟。
  5. 分布式处理

    • 如果任务量很大,确保 CheckTaskStatus 事件的消费者是分布式的,避免单点瓶颈。
    • Watermill 支持多消费者组,适合分布式场景。
  6. 回调替代轮询

    • 如果第三方接口支持 webhook 回调,可以让第三方直接推送结果到你的系统,发布 ThirdPartyAPIResult 事件,彻底避免状态检查。
    • 例如,配置一个 HTTP 端点接收回调,解析后发布事件。

扩展示例:指数退避

在 CheckTaskStatus 处理器中添加指数退避:

go

复制

// 在重新发布 CheckTaskStatus 之前 delay := time.Duration(100*(1<<event.RetryCount)) * time.Millisecond // 100ms, 200ms, 400ms, ... time.Sleep(delay)

参考资料

这个实现展示了如何在 Watermill 的 CQRS 架构中处理异步第三方接口的调用和状态检查,避免了 for 循环轮询,同时保持了系统的解耦和可扩展性。如果你的第三方接口支持 webhook 或有其他具体需求,可以进一步调整逻辑! 以下是一个完整的实例:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/ThreeDotsLabs/watermill"
	"github.com/ThreeDotsLabs/watermill/components/cqrs"
	"github.com/ThreeDotsLabs/watermill/message"
	"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
	"github.com/pkg/errors"
)

// SendWelcomeEmail 命令,触发发送欢迎邮件
type SendWelcomeEmail struct {
	UserID    string
	Email     string
	Timestamp time.Time
}

// ThirdPartyAPICalled 事件,表示异步接口已调用
type ThirdPartyAPICalled struct {
	UserID     string
	AttemptID  string
	TaskID     string
	Email      string
	Timestamp  time.Time
}

// CheckTaskStatus 事件,触发任务状态检查
type CheckTaskStatus struct {
	UserID     string
	AttemptID  string
	TaskID     string
	Email      string
	RetryCount int
	MaxRetries int
	Timestamp  time.Time
}

// EmailSent 事件,表示邮件发送成功
type EmailSent struct {
	UserID     string
	AttemptID  string
	TaskID     string
	Timestamp  time.Time
}

// EmailFailed 事件,表示邮件发送失败
type EmailFailed struct {
	UserID     string
	AttemptID  string
	TaskID     string
	Error      string
	Timestamp  time.Time
}

// 模拟第三方异步接口调用
func callThirdPartyAsyncAPI(email string) (string, error) {
	// 模拟快速返回任务 ID
	return watermill.NewUUID(), nil
}

// 模拟查询任务状态
func checkTaskStatus(taskID string) (string, error) {
	// 模拟状态:pending, completed, failed
	// 使用 taskID 的哈希模拟状态变化
	hash := int(taskID[0])
	if hash%2 == 0 {
		return "pending", nil
	}
	if hash%3 == 0 {
		return "completed", nil
	}
	return "failed", fmt.Errorf("task %s failed", taskID)
}

func main() {
	// 创建 Watermill 日志
	logger := watermill.NewStdLogger(false, false)

	// 使用 GoChannel 作为 Pub/Sub(生产环境可替换为 Kafka、RabbitMQ)
	pubSub := gochannel.NewGoChannel(
		gochannel.Config{},
		logger,
	)

	// 创建 CQRS 事件总线
	eventBus, err := cqrs.NewEventBusWithConfig(
		pubSub,
		cqrs.EventBusConfig{
			GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
				return "events." + params.EventName, nil
			},
			Marshaler: cqrs.JSONMarshaler{
				GenerateName: cqrs.StructName,
			},
		},
	)
	if err != nil {
		log.Fatalf("Failed to create event bus: %v", err)
	}

	// 创建 CQRS 命令总线
	commandBus, err := cqrs.NewCommandBusWithConfig(
		pubSub,
		cqrs.CommandBusConfig{
			GeneratePublishTopic: func(params cqrs.GenerateCommandPublishTopicParams) (string, error) {
				return "commands." + params.CommandName, nil
			},
			Marshaler: cqrs.JSONMarshaler{
				GenerateName: cqrs.StructName,
			},
		},
	)
	if err != nil {
		log.Fatalf("Failed to create command bus: %v", err)
	}

	// 创建命令处理器
	commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
		pubSub,
		cqrs.CommandProcessorConfig{
			GenerateSubscribeTopic: func(params cqrs.GenerateCommandSubscribeTopicParams) (string, error) {
				return "commands." + params.CommandName, nil
			},
			SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
				return pubSub, nil
			},
			Marshaler: cqrs.JSONMarshaler{
				GenerateName: cqrs.StructName,
			},
		},
	)
	if err != nil {
		log.Fatalf("Failed to create command processor: %v", err)
	}

	// 创建事件处理器
	eventProcessor, err := cqrs.NewEventProcessorWithConfig(
		pubSub,
		cqrs.EventProcessorConfig{
			GenerateSubscribeTopic: func(params cqrs.GenerateEventSubscribeTopicParams) (string, error) {
				return "events." + params.EventName, nil
			},
			SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
				return pubSub, nil
			},
			Marshaler: cqrs.JSONMarshaler{
				GenerateName: cqrs.StructName,
			},
		},
	)
	if err != nil {
		log.Fatalf("Failed to create event processor: %v", err)
	}

	// 处理 SendWelcomeEmail 命令
	commandProcessor.AddHandlers(
		cqrs.NewCommandHandler(
			"SendWelcomeEmail",
			func(ctx context.Context, cmd interface{}) ([]interface{}, error) {
				command := cmd.(*SendWelcomeEmail)
				// 触发异步接口调用
				taskID, err := callThirdPartyAsyncAPI(command.Email)
				if err != nil {
					return nil, errors.Wrap(err, "failed to call third-party API")
				}
				// 发布 ThirdPartyAPICalled 事件
				return []interface{}{
					&ThirdPartyAPICalled{
						UserID:    command.UserID,
						AttemptID: watermill.NewUUID(),
						TaskID:    taskID,
						Email:     command.Email,
						Timestamp: time.Now(),
					},
				}, nil
			},
		),
	)

	// 处理 ThirdPartyAPICalled 事件,触发状态检查
	eventProcessor.AddHandlers(
		cqrs.NewEventHandler(
			"ThirdPartyAPICalled",
			func(ctx context.Context, event interface{}) error {
				e := event.(*ThirdPartyAPICalled)
				// 发布 CheckTaskStatus 事件
				return eventBus.Publish(ctx, &CheckTaskStatus{
					UserID:     e.UserID,
					AttemptID:  e.AttemptID,
					TaskID:     e.TaskID,
					Email:      e.Email,
					RetryCount: 0,
					MaxRetries: 5,
					Timestamp:  time.Now(),
				})
			},
		),
	)

	// 处理 CheckTaskStatus 事件
	eventProcessor.AddHandlers(
		cqrs.NewEventHandler(
			"CheckTaskStatus",
			func(ctx context.Context, event interface{}) error {
				e := event.(*CheckTaskStatus)
				// 检查任务状态
				status, err := checkTaskStatus(e.TaskID)
				if err != nil {
					// 状态检查失败,发布 EmailFailed 事件
					return eventBus.Publish(ctx, &EmailFailed{
						UserID:    e.UserID,
						AttemptID: e.AttemptID,
						TaskID:    e.TaskID,
						Error:     fmt.Sprintf("Failed to check status: %v", err),
						Timestamp: time.Now(),
					})
				}

				if status == "completed" {
					// 任务完成,发布 EmailSent 事件
					return eventBus.Publish(ctx, &EmailSent{
						UserID:    e.UserID,
						AttemptID: e.AttemptID,
						TaskID:    e.TaskID,
						Timestamp: time.Now(),
					})
				}

				if status == "failed" {
					// 任务失败,发布 EmailFailed 事件
					return eventBus.Publish(ctx, &EmailFailed{
						UserID:    e.UserID,
						AttemptID: e.AttemptID,
						TaskID:    e.TaskID,
						Error:     fmt.Sprintf("Task %s failed", e.TaskID),
						Timestamp: time.Now(),
					})
				}

				// 如果任务仍在 pending 且未达到最大重试次数,延迟后重新发布 CheckTaskStatus
				if status == "pending" && e.RetryCount < e.MaxRetries {
					// 指数退避:100ms * 2^RetryCount
					delay := time.Duration(100*(1<<e.RetryCount)) * time.Millisecond
					time.Sleep(delay)
					e.RetryCount++
					e.Timestamp = time.Now()
					return eventBus.Publish(ctx, e)
				}

				// 达到最大重试次数,发布 EmailFailed 事件
				return eventBus.Publish(ctx, &EmailFailed{
					UserID:    e.UserID,
					AttemptID: e.AttemptID,
					TaskID:    e.TaskID,
					Error:     fmt.Sprintf("Task %s did not complete after %d retries", e.TaskID, e.MaxRetries),
					Timestamp: time.Now(),
				})
			},
		),
	)

	// 处理 EmailSent 和 EmailFailed 事件
	eventProcessor.AddHandlers(
		cqrs.NewEventHandler(
			"EmailSent",
			func(ctx context.Context, event interface{}) error {
				e := event.(*EmailSent)
				log.Printf("Email sent successfully for user %s (attempt %s, task %s)", e.UserID, e.AttemptID, e.TaskID)
				return nil
			},
		),
		cqrs.NewEventHandler(
			"EmailFailed",
			func(ctx context.Context, event interface{}) error {
				e := event.(*EmailFailed)
				log.Printf("Email failed for user %s (attempt %s, task %s): %s", e.UserID, e.AttemptID, e.TaskID, e.Error)
				return nil
			},
		),
	)

	// 启动命令和事件处理器
	go func() {
		if err := commandProcessor.Run(context.Background()); err != nil {
			log.Fatalf("Failed to run command processor: %v", err)
		}
	}()
	go func() {
		if err := eventProcessor.Run(context.Background()); err != nil {
			log.Fatalf("Failed to run event processor: %v", err)
		}
	}()

	// 模拟发送 SendWelcomeEmail 命令
	ctx := context.Background()
	err = commandBus.Send(ctx, &SendWelcomeEmail{
		UserID:    "user123",
		Email:     "test@example.com",
		Timestamp: time.Now(),
	})
	if err != nil {
		log.Fatalf("Failed to send command: %v", err)
	}

	// 保持程序运行
	select {}
}