在使用 Go 的 Watermill 库进行 CQRS 和事件驱动开发时,异步调用第三方接口并处理结果是一个常见的场景。在事件创建后,通过一个事件来触发异步调用,并通过某种机制(如 for 循环)监听调用结果,同时处理超时逻辑。这种方式在实际开发中可能不是最优的,因为 Watermill 的事件驱动模型更倾向于通过发布/订阅(Pub/Sub)机制来解耦事件的生产和消费,而不建议在事件处理中直接使用 for 循环来轮询结果。
以下是一个更符合 Watermill 设计理念的实现方式:
- 事件触发:当某个业务事件发生时,发布一个事件(例如
CallThirdPartyAPI)。 - 事件处理器:订阅该事件,在处理器中异步调用第三方接口。
- 结果处理:调用完成后,发布一个新的事件(如
ThirdPartyAPIResult)来表示结果或超时状态。 - 超时控制:使用 Go 的
context包来管理超时逻辑。 - 解耦:避免在事件处理器中使用 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 {}
}
代码说明
-
事件定义:
UserRegistered:表示用户注册的事件,触发后续逻辑。CallThirdPartyAPI:表示需要调用第三方接口的事件,包含用户 ID 和邮件地址等信息。ThirdPartyAPIResult:表示第三方接口调用的结果,包含成功/失败状态和错误信息。
-
Watermill Pub/Sub:
- 使用
gochannel.NewGoChannel作为消息通道(生产环境中可替换为 Kafka 或 RabbitMQ)。 - 发布和订阅事件通过
publisher.Publish和pubSub.Subscribe实现。
- 使用
-
异步调用与超时:
- 在处理
CallThirdPartyAPI事件时,使用 Go 协程异步调用第三方接口(callThirdPartyAPI)。 - 使用
context.WithTimeout设置 2 秒超时。 - 通过
select监听调用结果或超时,超时后发布包含错误信息的ThirdPartyAPIResult事件。
- 在处理
-
结果处理:
- 订阅
third_party_api_result事件,处理调用结果(成功或失败)。 - 使用
log.Printf输出结果,实际场景中可以更新数据库或执行其他操作。
- 订阅
-
避免 for 循环轮询:
- 没有使用 for 循环来轮询结果,而是通过事件驱动的方式,利用 Watermill 的 Pub/Sub 机制来传递和处理结果。
- 这种方式更符合 CQRS 和事件驱动架构的解耦理念。
运行说明
-
依赖安装:
- 确保已安装 Watermill 库:
go get github.com/ThreeDotsLabs/watermill go get github.com/ThreeDotsLabs/watermill-amqp - 本例使用
gochannel作为 Pub/Sub 后端,无需额外依赖。生产环境中可替换为 Kafka 或 RabbitMQ。
- 确保已安装 Watermill 库:
-
运行程序:
- 运行
go run main.go,程序会模拟用户注册,触发事件链:UserRegistered→CallThirdPartyAPI→ThirdPartyAPIResult。
- 日志会显示邮件发送的成功或失败(包括超时)。
- 运行
-
输出示例:
- 成功案例:
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 资源,尤其在高并发场景下。
相反,使用 context 和 select 结合异步协程可以:
- 优雅地处理超时。
- 保持事件处理器的非阻塞性。
- 通过 Watermill 的事件机制解耦调用和结果处理。
扩展建议
-
重试机制:
- 如果第三方接口调用失败,可以发布一个
RetryThirdPartyAPI事件,包含重试次数,配合指数退避策略。
- 如果第三方接口调用失败,可以发布一个
-
持久化:
- 将事件存储到数据库(如 Event Sourcing),以便追踪和审计。
-
生产环境:
- 替换
gochannel为 Kafka 或 RabbitMQ(Watermill 支持多种后端)。 - 使用 Watermill 的 CQRS 组件(如
github.com/ThreeDotsLabs/watermill/components/cqrs)来进一步简化命令和事件处理。
- 替换
-
错误处理:
- 在生产环境中,添加监控和告警机制,记录失败的调用。
参考资料
- Watermill 官方文档:watermill.io/[](watermill.io/docs/cqrs/)…](watermill.io/docs/gettin…)
- Watermill GitHub:github.com/ThreeDotsLa…](github.com/ThreeDotsLa…)
这个实现展示了如何在 Watermill 中结合 CQRS 和事件驱动架构处理异步第三方接口调用,同时保持代码的清晰和可扩展性。如果有其他具体需求(例如重试逻辑或特定第三方接口),可以进一步定制! 当第三方接口是异步的(即接口调用快速返回,但实际任务的执行结果需要通过轮询或回调机制获取),在 Watermill 的 CQRS 和事件驱动架构中,我们需要一种方式来跟踪任务的最终状态,而不依赖 for 循环轮询以保持事件处理器的非阻塞性和解耦性。以下是解决这个问题的推荐方法:
解决方案概述
- 触发异步调用:在处理某个事件(如
CallThirdPartyAPI)时,调用第三方异步接口,获取一个任务 ID。 - 发布状态查询事件:接口返回任务 ID 后,发布一个新事件(如
CheckTaskStatus),包含任务 ID 和相关元数据。 - 异步状态检查:订阅
CheckTaskStatus事件,定期检查任务状态(通过第三方接口提供的状态查询接口),并在必要时重新发布CheckTaskStatus事件以实现延迟重试。 - 结果处理:当任务状态表明完成(成功或失败)时,发布
ThirdPartyAPIResult事件,供后续处理器消费。 - 超时控制:使用
context或事件元数据跟踪超时,避免无限检查。 - 避免 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 {}
}
代码说明
-
事件定义:
- UserRegistered:用户注册事件,触发异步调用流程。
- CallThirdPartyAPI:触发异步接口调用的事件。
- CheckTaskStatus:检查任务状态的事件,包含任务 ID、重试次数和最大重试次数。
- ThirdPartyAPIResult:最终结果事件,记录任务的成功或失败。
-
异步接口调用:
- callThirdPartyAsyncAPI 模拟第三方异步接口,返回任务 ID。
- 处理器在收到 CallThirdPartyAPI 事件后调用接口,并发布 CheckTaskStatus 事件。
-
状态检查:
-
checkTaskStatus 模拟查询任务状态,返回 "pending"、"completed" 或 "failed"。
-
在 CheckTaskStatus 处理器中:
- 如果状态是 "completed",发布成功的结果事件。
- 如果状态是 "failed",发布失败的结果事件。
- 如果状态是 "pending" 且未达到最大重试次数,延迟 1 秒后重新发布 CheckTaskStatus 事件。
- 如果达到最大重试次数,发布失败的结果事件。
-
-
超时和重试:
- 通过 RetryCount 和 MaxRetries 控制重试次数,避免无限检查。
- 使用 time.Sleep 模拟延迟,生产环境中可以使用更精确的定时器(如 time.After)或消息队列的延迟功能(如 Kafka 的延迟主题)。
-
避免 for 循环:
- 通过事件驱动的方式,CheckTaskStatus 事件在每次检查后决定是否重新发布自身,形成一个“事件循环”,代替阻塞的 for 循环。
- 这种方式保持了事件处理器的非阻塞性,且符合 Watermill 的解耦设计。
-
结果处理:
- ThirdPartyAPIResult 事件的处理器记录任务的最终状态(成功或失败)。
- 实际场景中,可以将结果存储到数据库或触发其他业务逻辑。
运行说明
-
依赖安装:
bash
复制
go get github.com/ThreeDotsLabs/watermill本例使用 gochannel 作为 Pub/Sub 后端,生产环境中可替换为 Kafka 或 RabbitMQ。
-
运行程序:
-
运行 go run main.go,程序会模拟用户注册,触发事件链:
- UserRegistered → CallThirdPartyAPI → CheckTaskStatus → ThirdPartyAPIResult。
-
日志会显示任务状态的检查和最终结果。
-
-
输出示例:
-
成功案例:
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 或延迟时间来控制检查频率。
生产环境优化
-
延迟机制:
- 使用消息队列的延迟功能(如 RabbitMQ 的 TTL 或 Kafka 的延迟主题)代替 time.Sleep。
- 例如,Watermill 支持 RabbitMQ,可以设置消息的 TTL。
-
指数退避:
- 在 CheckTaskStatus 处理器中,增加指数退避策略,减少对第三方接口的频繁调用。
- 示例:延迟时间可以是 baseDelay * (2 ^ RetryCount)。
-
持久化:
- 将 CheckTaskStatus 和 ThirdPartyAPIResult 事件存储到数据库(如 Event Sourcing),以便追踪任务状态和审计。
-
监控和告警:
- 记录每次状态检查的日志,集成监控系统(如 Prometheus)以跟踪失败率和延迟。
-
分布式处理:
- 如果任务量很大,确保 CheckTaskStatus 事件的消费者是分布式的,避免单点瓶颈。
- Watermill 支持多消费者组,适合分布式场景。
-
回调替代轮询:
- 如果第三方接口支持 webhook 回调,可以让第三方直接推送结果到你的系统,发布 ThirdPartyAPIResult 事件,彻底避免状态检查。
- 例如,配置一个 HTTP 端点接收回调,解析后发布事件。
扩展示例:指数退避
在 CheckTaskStatus 处理器中添加指数退避:
go
复制
// 在重新发布 CheckTaskStatus 之前 delay := time.Duration(100*(1<<event.RetryCount)) * time.Millisecond // 100ms, 200ms, 400ms, ... time.Sleep(delay)
参考资料
- Watermill 官方文档:watermill.io/
- Watermill GitHub:github.com/ThreeDotsLa…
- Go context 包:pkg.go.dev/context
这个实现展示了如何在 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 {}
}