191 lines
6.4 KiB
Go
191 lines
6.4 KiB
Go
package domain
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"time"
|
||
)
|
||
|
||
// ==================== P0-06 Outbox模式实现 ====================
|
||
|
||
// Outbox 事务性说明:
|
||
// 1. 在业务事务内:当账户创建、套餐变更、结算发生时,将领域事件写入 outbox 表
|
||
// 2. 事务保证:outbox 事件与业务数据在同一个数据库事务中提交,确保原子性
|
||
// 3. 异步处理:OutboxProcessor 定期扫描 outbox 表,发布事件到消息队列
|
||
// 4. 幂等发布:处理器通过 FetchAndLock 实现分布式锁,确保每条消息只被处理一次
|
||
// 5. 失败重试:发布失败的事件会根据退避策略重试,最大重试次数为 DefaultMaxRetries
|
||
// 6. 死信处理:超过最大重试次数的事件移入 dead_letter 表,供人工处理
|
||
|
||
// OutboxEvent Outbox事件
|
||
type OutboxEvent struct {
|
||
ID int64 `json:"id"`
|
||
AggregateType string `json:"aggregate_type"`
|
||
AggregateID string `json:"aggregate_id"`
|
||
EventType string `json:"event_type"`
|
||
EventID string `json:"event_id"`
|
||
Payload json.RawMessage `json:"payload"`
|
||
Status string `json:"status"` // pending, processing, completed, failed, dead_letter
|
||
RetryCount int `json:"retry_count"`
|
||
MaxRetries int `json:"max_retries"`
|
||
ErrorMessage string `json:"error_message,omitempty"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
ProcessedAt *time.Time `json:"processed_at,omitempty"`
|
||
NextRetryAt *time.Time `json:"next_retry_at,omitempty"`
|
||
DeadLetterReason string `json:"dead_letter_reason,omitempty"`
|
||
Version int64 `json:"version"`
|
||
}
|
||
|
||
// OutboxStatus Outbox事件状态
|
||
const (
|
||
OutboxStatusPending = "pending"
|
||
OutboxStatusProcessing = "processing"
|
||
OutboxStatusCompleted = "completed"
|
||
OutboxStatusFailed = "failed"
|
||
OutboxStatusDeadLetter = "dead_letter"
|
||
)
|
||
|
||
// OutboxDeadLetter 死信记录
|
||
type OutboxDeadLetter struct {
|
||
ID int64 `json:"id"`
|
||
OriginalEventID string `json:"original_event_id"`
|
||
OriginalAggregateType string `json:"original_aggregate_type"`
|
||
OriginalAggregateID string `json:"original_aggregate_id"`
|
||
EventType string `json:"event_type"`
|
||
Payload json.RawMessage `json:"payload"`
|
||
ErrorMessage string `json:"error_message,omitempty"`
|
||
RetryCount int `json:"retry_count"`
|
||
FirstFailedAt time.Time `json:"first_failed_at"`
|
||
DeadLetterAt time.Time `json:"dead_letter_at"`
|
||
Handled bool `json:"handled"`
|
||
HandledAt *time.Time `json:"handled_at,omitempty"`
|
||
HandlerNotes string `json:"handler_notes,omitempty"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// OutboxProcessor Outbox处理器
|
||
type OutboxProcessor struct {
|
||
eventStore OutboxEventStore
|
||
messageBroker MessageBroker
|
||
stats OutboxStats
|
||
}
|
||
|
||
// OutboxEventStore Outbox事件存储接口
|
||
type OutboxEventStore interface {
|
||
// FetchAndLock 获取并锁定待处理事件
|
||
FetchAndLock(ctx context.Context, limit int) ([]*OutboxEvent, error)
|
||
// MarkCompleted 标记完成
|
||
MarkCompleted(ctx context.Context, eventID string) error
|
||
// MarkFailed 标记失败
|
||
MarkFailed(ctx context.Context, eventID string, errorMsg string) error
|
||
// MoveToDeadLetter 移入死信队列
|
||
MoveToDeadLetter(ctx context.Context, event *OutboxEvent, errorMsg string) error
|
||
}
|
||
|
||
// MessageBroker 消息代理接口
|
||
type MessageBroker interface {
|
||
// Publish 发布消息
|
||
Publish(ctx context.Context, event *OutboxEvent) error
|
||
}
|
||
|
||
// OutboxStats Outbox统计
|
||
type OutboxStats interface {
|
||
RecordOutboxSuccess(eventType string)
|
||
RecordOutboxFailure(reason string)
|
||
RecordOutboxRetry(eventType string)
|
||
RecordOutboxDLQ(eventType string)
|
||
}
|
||
|
||
// DefaultMaxRetries 默认最大重试次数
|
||
const DefaultMaxRetries = 5
|
||
|
||
// DefaultInitialBackoffSeconds 默认初始退避时间(秒)
|
||
const DefaultInitialBackoffSeconds = 1
|
||
|
||
// DefaultMaxBackoffSeconds 默认最大退避时间(秒)
|
||
const DefaultMaxBackoffSeconds = 60
|
||
|
||
// ProcessOutbox 处理Outbox事件
|
||
func (p *OutboxProcessor) ProcessOutbox(ctx context.Context) error {
|
||
// 1. 获取待处理事件
|
||
events, err := p.eventStore.FetchAndLock(ctx, 100)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to fetch outbox events: %w", err)
|
||
}
|
||
|
||
for _, event := range events {
|
||
// 2. 发布消息
|
||
if err := p.messageBroker.Publish(ctx, event); err != nil {
|
||
// 3. 处理失败
|
||
p.handleFailure(ctx, event, err)
|
||
continue
|
||
}
|
||
|
||
// 4. 标记完成
|
||
if err := p.eventStore.MarkCompleted(ctx, event.EventID); err != nil {
|
||
p.stats.RecordOutboxFailure("mark_completed_failed")
|
||
continue
|
||
}
|
||
|
||
p.stats.RecordOutboxSuccess(event.EventType)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// handleFailure 处理失败事件
|
||
func (p *OutboxProcessor) handleFailure(ctx context.Context, event *OutboxEvent, publishErr error) {
|
||
event.RetryCount++
|
||
event.ErrorMessage = publishErr.Error()
|
||
|
||
if event.RetryCount >= event.MaxRetries {
|
||
// 移入死信队列
|
||
if err := p.eventStore.MoveToDeadLetter(ctx, event, publishErr.Error()); err != nil {
|
||
p.stats.RecordOutboxFailure("move_to_dlq_failed")
|
||
} else {
|
||
p.stats.RecordOutboxDLQ(event.EventType)
|
||
}
|
||
} else {
|
||
// 计算下次重试时间(指数退避)
|
||
backoffSeconds := CalculateOutboxBackoff(event.RetryCount, event.MaxRetries)
|
||
nextRetry := time.Now().Add(time.Duration(backoffSeconds) * time.Second)
|
||
|
||
// 在存储层更新重试状态(这里简化处理)
|
||
if err := p.eventStore.MarkFailed(ctx, event.EventID, publishErr.Error()); err != nil {
|
||
p.stats.RecordOutboxFailure("mark_failed_failed")
|
||
} else {
|
||
p.stats.RecordOutboxRetry(event.EventType)
|
||
}
|
||
|
||
event.NextRetryAt = &nextRetry
|
||
}
|
||
}
|
||
|
||
// CalculateOutboxBackoff 计算指数退避时间
|
||
func CalculateOutboxBackoff(retryCount, maxRetries int) int {
|
||
backoff := DefaultInitialBackoffSeconds * int(math.Pow(2, float64(retryCount-1)))
|
||
if backoff > DefaultMaxBackoffSeconds {
|
||
backoff = DefaultMaxBackoffSeconds
|
||
}
|
||
return backoff
|
||
}
|
||
|
||
// OutboxProcessorConfig Outbox处理器配置
|
||
type OutboxProcessorConfig struct {
|
||
MaxRetries int
|
||
InitialBackoffSeconds int
|
||
MaxBackoffSeconds int
|
||
BatchSize int
|
||
}
|
||
|
||
// DefaultOutboxProcessorConfig 默认配置
|
||
func DefaultOutboxProcessorConfig() *OutboxProcessorConfig {
|
||
return &OutboxProcessorConfig{
|
||
MaxRetries: DefaultMaxRetries,
|
||
InitialBackoffSeconds: DefaultInitialBackoffSeconds,
|
||
MaxBackoffSeconds: DefaultMaxBackoffSeconds,
|
||
BatchSize: 100,
|
||
}
|
||
}
|