进阶项目:基于 JetStream 的订单处理系统
本项目实现一个基于 NATS JetStream 的持久化订单处理系统,支持消息持久化、消息确认、消费者组和消息重试。
4.1 项目概述
功能需求
- 使用 JetStream 持久化订单消息
- 实现消息的可靠投递和确认
- 实现消费者组负载均衡
- 实现消息重试机制
- 实现消息去重
- 实现订单状态管理
项目结构
order-processing/
├── cmd/
│ ├── producer/main.go # 订单生产者
│ ├── consumer/main.go # 订单消费者
│ └── admin/main.go # 管理工具
├── internal/
│ ├── order/
│ │ ├── order.go # 订单模型
│ │ └── repository.go # 订单仓库
│ ├── stream/
│ │ └── manager.go # Stream 管理
│ ├── consumer/
│ │ └── processor.go # 消费者处理
│ └── metrics/
│ └── metrics.go # 指标监控
├── config.yaml
└── go.mod4.2 订单模型
go
// internal/order/order.go
package order
import (
"encoding/json"
"errors"
"time"
)
type OrderStatus string
const (
StatusPending OrderStatus = "pending"
StatusPaid OrderStatus = "paid"
StatusProcessing OrderStatus = "processing"
StatusCompleted OrderStatus = "completed"
StatusCancelled OrderStatus = "cancelled"
StatusFailed OrderStatus = "failed"
)
// Order 订单
type Order struct {
ID string `json:"id"`
CustomerID string `json:"customer_id"`
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
Quantity int `json:"quantity"`
Amount float64 `json:"amount"`
Status OrderStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PaidAt *time.Time `json:"paid_at,omitempty"`
}
// NewOrder 创建新订单
func NewOrder(customerID, productID, productName string, quantity int, amount float64) *Order {
now := time.Now()
return &Order{
ID: generateOrderID(),
CustomerID: customerID,
ProductID: productID,
ProductName: productName,
Quantity: quantity,
Amount: amount,
Status: StatusPending,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证订单
func (o *Order) Validate() error {
if o.CustomerID == "" {
return errors.New("客户ID不能为空")
}
if o.ProductID == "" {
return errors.New("商品ID不能为空")
}
if o.Quantity <= 0 {
return errors.New("数量必须大于0")
}
if o.Amount <= 0 {
return errors.New("金额必须大于0")
}
return nil
}
// ToJSON 序列化为 JSON
func (o *Order) ToJSON() ([]byte, error) {
return json.Marshal(o)
}
// FromJSON 从 JSON 反序列化
func FromJSON(data []byte) (*Order, error) {
var order Order
err := json.Unmarshal(data, &order)
return &order, err
}
// UpdateStatus 更新订单状态
func (o *Order) UpdateStatus(status OrderStatus) {
o.Status = status
o.UpdatedAt = time.Now()
if status == StatusPaid {
now := time.Now()
o.PaidAt = &now
}
}
func generateOrderID() string {
return time.Now().Format("20060102150405") + "-" + randomString(8)
}
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
}
return string(b)
}4.3 Stream 管理
go
// internal/stream/manager.go
package stream
import (
"context"
"fmt"
"log"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
// Manager JetStream Stream 管理器
type Manager struct {
js jetstream.JetStream
nc *nats.Conn
}
// New 创建 Stream 管理器
func New(url string) (*Manager, error) {
nc, err := nats.Connect(url)
if err != nil {
return nil, fmt.Errorf("连接 NATS 失败: %w", err)
}
js, err := jetstream.New(nc)
if err != nil {
return nil, fmt.Errorf("创建 JetStream 失败: %w", err)
}
return &Manager{js: js, nc: nc}, nil
}
// Close 关闭连接
func (m *Manager) Close() {
if m.nc != nil {
m.nc.Close()
}
}
// CreateOrderStream 创建订单 Stream
func (m *Manager) CreateOrderStream(ctx context.Context) error {
// 检查是否已存在
stream, err := m.js.Stream(ctx, "ORDERS")
if err == nil {
info, _ := stream.CachedInfo()
log.Printf("Stream ORDERS 已存在,消息数: %d", info.State.Msgs)
return nil
}
// 创建 Stream
stream, err = m.js.CreateStream(ctx, jetstream.StreamConfig{
Name: "ORDERS",
Subjects: []string{"orders.>"},
Storage: jetstream.FileStorage,
// 保留策略
Retention: jetstream.LimitsPolicy{
MaxBytes: 1024 * 1024 * 1024, // 1GB
MaxAge: time.Hour * 24 * 7, // 7 天
MaxMsgs: 100000,
},
// 丢弃策略
Discard: jetstream.DiscardOld,
// 副本数
Replicas: 1,
// 允许直接访问
AllowDirect: true,
})
if err != nil {
return fmt.Errorf("创建 Stream 失败: %w", err)
}
log.Printf("创建 Stream 成功: %s", stream.CachedInfo().Config.Name)
return nil
}
// CreateConsumer 创建消费者
func (m *Manager) CreateConsumer(ctx context.Context, name, subject string) (jetstream.Consumer, error) {
consumer, err := m.js.CreateConsumer(ctx, "ORDERS", jetstream.ConsumerConfig{
Name: name,
Durable: true,
FilterSubject: subject,
DeliverPolicy: jetstream.DeliverAll,
// 确认策略
AckPolicy: jetstream.AckExplicit,
AckWait: 30 * time.Second,
MaxDeliver: 3,
// 背压策略
MaxAckPending: 100,
// 重试策略
BackOff: []time.Duration{
time.Second,
time.Second * 5,
time.Second * 30,
},
// 过滤器
FilterSubject: subject,
})
if err != nil {
return nil, fmt.Errorf("创建消费者失败: %w", err)
}
return consumer, nil
}
// GetStreamInfo 获取 Stream 信息
func (m *Manager) GetStreamInfo(ctx context.Context) (*jetstream.StreamInfo, error) {
stream, err := m.js.Stream(ctx, "ORDERS")
if err != nil {
return nil, err
}
return stream.CachedInfo(), nil
}
// ListConsumers 列出所有消费者
func (m *Manager) ListConsumers(ctx context.Context) ([]jetstream.ConsumerInfo, error) {
return m.js.ListConsumers(ctx, "ORDERS")
}
// PurgeStream 清空 Stream
func (m *Manager) PurgeStream(ctx context.Context) error {
stream, err := m.js.Stream(ctx, "ORDERS")
if err != nil {
return err
}
return stream.Purge(ctx)
}4.4 订单生产者
go
// cmd/producer/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"order-processing/internal/order"
"order-processing/internal/stream"
"github.com/nats-io/nats.go/jetstream"
)
type Producer struct {
js jetstream.JetStream
ctx context.Context
}
func NewProducer(url string) (*Producer, error) {
mgr, err := stream.New(url)
if err != nil {
return nil, err
}
js, err := jetstream.New(mgr.GetNATSConn())
if err != nil {
return nil, err
}
return &Producer{
js: js,
ctx: context.Background(),
}, nil
}
func (p *Producer) PublishOrder(order *order.Order) error {
data, err := order.ToJSON()
if err != nil {
return err
}
// 使用消息 ID 实现幂等
msgID := fmt.Sprintf("%s-%d", order.ID, time.Now().UnixNano())
_, err = p.js.Publish(p.ctx, "orders.created", data,
jetstream.WithMsgID(msgID),
jetstream.WithHeader(nats.Header{
"Content-Type": []string{"application/json"},
"Order-Status": []string{string(order.Status)},
}),
)
return err
}
func (p *Producer) PublishOrderUpdate(order *order.Order) error {
data, err := order.ToJSON()
if err != nil {
return err
}
subject := fmt.Sprintf("orders.%s", order.ID)
_, err = p.js.Publish(p.ctx, subject, data)
return err
}
func main() {
producer, err := NewProducer(nats.DefaultURL)
if err != nil {
log.Fatal(err)
}
// 创建 Stream
if err := producer.js.CreateStream(producer.ctx, jetstream.StreamConfig{
Name: "ORDERS",
Subjects: []string{"orders.>"},
Storage: jetstream.FileStorage,
}); err != nil {
log.Printf("创建 Stream: %v", err)
}
log.Println("生产者已启动")
// 产品列表
products := []struct {
id, name string
price float64
}{
{"P001", "iPhone 15", 999.0},
{"P002", "MacBook Pro", 1999.0},
{"P003", "iPad Air", 599.0},
{"P004", "AirPods Pro", 249.0},
}
// 监听信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
i := 0
for {
select {
case <-ticker.C:
product := products[i%len(products)]
ord := order.NewOrder(
fmt.Sprintf("C%03d", i%10+1),
product.id,
product.name,
1,
product.price,
)
if err := producer.PublishOrder(ord); err != nil {
log.Printf("发布订单失败: %v", err)
} else {
log.Printf("已发布订单: %s, %s, %.2f", ord.ID, ord.ProductName, ord.Amount)
}
i++
case <-sigCh:
log.Println("收到退出信号")
return
}
}
}4.5 订单消费者
go
// internal/consumer/processor.go
package consumer
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/nats-io/nats.go/jetstream"
"order-processing/internal/order"
)
// Processor 订单处理器
type Processor struct {
name string
consumer jetstream.Consumer
wg sync.WaitGroup
running bool
}
// New 创建处理器
func New(name string, consumer jetstream.Consumer) *Processor {
return &Processor{
name: name,
consumer: consumer,
}
}
// Start 启动处理
func (p *Processor) Start(ctx context.Context, workers int) {
p.running = true
for i := 0; i < workers; i++ {
p.wg.Add(1)
go p.worker(ctx, i)
}
log.Printf("[%s] 启动了 %d 个 worker", p.name, workers)
}
// Stop 停止处理
func (p *Processor) Stop() {
p.running = false
p.wg.Wait()
log.Printf("[%s] 已停止", p.name)
}
func (p *Processor) worker(ctx context.Context, id int) {
defer p.wg.Done()
for p.running {
select {
case <-ctx.Done():
return
default:
}
// 拉取消息
msgs, err := p.consumer.Fetch(1, jetstream.FetchMaxWait(5*time.Second))
if err != nil {
if err == context.Canceled {
return
}
log.Printf("[%s Worker-%d] 获取消息失败: %v", p.name, id, err)
time.Sleep(time.Second)
continue
}
for msg := range msgs {
if err := p.processMessage(msg); err != nil {
log.Printf("[%s Worker-%d] 处理失败: %v", p.name, id, err)
}
}
}
}
func (p *Processor) processMessage(msg jetstream.Msg) error {
// 解析订单
ord, err := order.FromJSON(msg.Data())
if err != nil {
log.Printf("[%s] 解析订单失败: %v", p.name, err)
msg.Term() // 终止消息,不再重试
return err
}
log.Printf("[%s] 收到订单: %s, 状态: %s", p.name, ord.ID, ord.Status)
// 模拟业务处理
time.Sleep(500 * time.Millisecond)
// 更新订单状态
switch ord.Status {
case order.StatusPending:
ord.UpdateStatus(order.StatusPaid)
case order.StatusPaid:
ord.UpdateStatus(order.StatusProcessing)
case order.StatusProcessing:
ord.UpdateStatus(order.StatusCompleted)
}
log.Printf("[%s] 处理完成: %s -> %s", p.name, ord.ID, ord.Status)
// 确认消息
return msg.Ack()
}
// GetMetrics 获取处理指标
func (p *Processor) GetMetrics() map[string]interface{} {
info, _ := p.consumer.CachedInfo()
return map[string]interface{}{
"name": p.name,
"pending": info.NumPending,
"delivered": info.NumDelivered,
"ack_wait": info.AckWait.Nanoseconds(),
"max_deliver": info.MaxDeliver,
}
}go
// cmd/consumer/main.go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"order-processing/internal/consumer"
"order-processing/internal/stream"
"github.com/nats-io/nats.go"
)
func main() {
// 创建 Stream 管理器
mgr, err := stream.New(nats.DefaultURL)
if err != nil {
log.Fatal(err)
}
defer mgr.Close()
ctx := context.Background()
// 创建 Stream
if err := mgr.CreateOrderStream(ctx); err != nil {
log.Printf("创建 Stream: %v", err)
}
// 创建消费者
consumerName := "order-processor"
consumer, err := mgr.CreateConsumer(ctx, consumerName, "orders.created")
if err != nil {
log.Printf("创建消费者: %v", err)
}
// 创建处理器
proc := consumer.NewProcessor(consumerName, consumer)
// 启动处理
ctx, cancel := context.WithCancel(context.Background())
proc.Start(ctx, 3)
// 监听信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
// 定期打印指标
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
metrics := proc.GetMetrics()
log.Printf("消费者指标: %+v", metrics)
case <-ctx.Done():
return
}
}
}()
<-sigCh
log.Println("收到退出信号,正在关闭...")
cancel()
proc.Stop()
log.Println("已关闭")
}4.6 管理工具
go
// cmd/admin/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"order-processing/internal/stream"
"github.com/nats-io/nats.go"
)
func main() {
mgr, err := stream.New(nats.DefaultURL)
if err != nil {
log.Fatal(err)
}
defer mgr.Close()
ctx := context.Background()
// 获取 Stream 信息
info, err := mgr.GetStreamInfo(ctx)
if err != nil {
log.Printf("获取 Stream 信息失败: %v", err)
os.Exit(1)
}
fmt.Println("=== Stream 信息 ===")
fmt.Printf("名称: %s\n", info.Config.Name)
fmt.Printf("主题: %v\n", info.Config.Subjects)
fmt.Printf("消息数: %d\n", info.State.Msgs)
fmt.Printf("字节数: %d\n", info.State.Bytes)
fmt.Printf("消费者数: %d\n", info.State.Consumers)
// 列出消费者
consumers, err := mgr.ListConsumers(ctx)
if err != nil {
log.Printf("列出消费者失败: %v", err)
} else {
fmt.Println("\n=== 消费者列表 ===")
for _, c := range consumers {
fmt.Printf("名称: %s, 待处理: %d, 已投递: %d\n",
c.Name, c.NumPending, c.NumDelivered)
}
}
}4.7 配置文件
yaml
# config.yaml
nats:
url: "nats://localhost:4222"
timeout: 10s
stream:
name: "ORDERS"
subjects:
- "orders.>"
storage: "file"
max_bytes: 1073741824 # 1GB
max_age: 604800 # 7 天
max_msgs: 100000
consumer:
name: "order-processor"
workers: 3
prefetch: 10
ack_wait: 30s
max_deliver: 3
max_ack_pending: 100
retry:
backoff:
- 1s
- 5s
- 30s4.8 运行项目
启动 NATS 服务器
bash
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 nats:2.11.10 -js启动生产者
bash
cd cmd/producer
go run main.go启动消费者
bash
cd cmd/consumer
go run main.go启动管理工具
bash
cd cmd/admin
go run main.go4.9 测试验证
查看 Stream 状态
bash
go run cmd/admin/main.go
# 输出示例
=== Stream 信息 ===
名称: ORDERS
主题: [orders.>]
消息数: 156
字节数: 24567
消费者数: 1
=== 消费者列表 ===
名称: order-processor, 待处理: 0, 已投递: 156查看监控面板
bash
# 访问 NATS 监控页面
http://localhost:8222/streamz4.10 扩展练习
- 添加数据库持久化:将订单存储到 PostgreSQL
- 添加消息转换:支持 Protobuf 格式
- 添加分布式事务:实现订单创建和支付的分布式事务
- 添加死信队列:处理失败的消息
- 添加监控告警:集成 Prometheus 和 Alertmanager