每日一Go-57、Go + NATS:微服务时代,消息队列的最优解之一

0 阅读4分钟

一、什么是NATS?

NATS是为分布式系统而生的超轻量、高性能消息系统。如果说Kafka是数据仓库型MQ,RabbitMQ是业务规则型MQ;那么NATS更像是微服务的神经系统。

二、为什么Go和NATS是天生一对

1. 同源基因

NATS用Go写的;官方客户端Go是一等公民。

2. 极致性能

指标NATS
延迟微秒级
吞吐百万级msg/s
占用几十MB内存
启动秒级

非常适合:RPC解耦;服务间事件通知;实时系统;游戏/金融/loT

三、NATS适合哪些场景?

1. 适合的场景

微服务之间的事件驱动

请求-响应

实时推送(直播、IM、比赛)

服务发现/控制信号

高并发、低延迟系统

2. 不适合的场景

离线大数据分析

强事务一致性

TB级消息堆积

四、JetStream:让NATS能落盘

JetStream解决了什么?

能力支持
消息持久化
ACK/重试
顺序消费
消费位点
类Kafka能力

生产级系统,JetStream是必选项。

五、NATS vs Kafka vs RabbitMQ

对比项NATSKafkaRabbitMQ
延迟⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
吞吐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
运维复杂度⭐⭐⭐⭐⭐⭐⭐⭐⭐
Go友好度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐    
微服务适配

总结起来就是:微服务用NATS,大数据用Kafka,复杂业务流用RabbitMQ。

六、实战一下

1. 封装nats组件

package components
import (
    "time"
    "github.com/nats-io/nats.go"
    "github.com/spf13/viper"
    "go.uber.org/zap"
)
// NatsBroker NATS消息队列代理
// 封装了NATS连接和JetStream上下文,提供消息发布订阅功能
type NatsBroker struct {
    NC *nats.Conn            // NATS连接对象
    JS nats.JetStreamContext // JetStream上下文,支持持久化消息
}
// NewNatsBroker 创建并初始化NATS消息队列代理
// 从配置文件读取NATS连接参数,建立连接并初始化JetStream
// 返回初始化完成的NatsBroker实例和可能的错误
func NewNatsBroker() (*NatsBroker, error) {
    url := viper.GetString("nats.url")
    // 1. 建立连接
    nc, err := nats.Connect(url,
        nats.MaxReconnects(10),            // 最大重连次数
        nats.ReconnectWait(2*time.Second), // 重连等待时间
        nats.DisconnectErrHandler(func(_ *nats.Conn, err error) {
            zap.L().Panic("NATS Disconnected:", zap.String("url", url))
        }),
    )
    if err != nil {
        return nil, err
    }
    // 2. 启用 JetStream
    js, err := nc.JetStream()
    if err != nil {
        return nil, err
    }
    // 3. 预定义 Stream (如果不存在则创建)
    // 就像 Kafka 的 Topic 配置
    _name := viper.GetString("nats.stream.name")
    _subjects := viper.GetStringSlice("nats.stream.subjects")
    _max_age := viper.GetDuration("nats.stream.max_age")
    _, err = js.AddStream(&nats.StreamConfig{
        Name:     _name,
        Subjects: _subjects,
        Storage:  nats.FileStorage, // 消息持久化到磁盘
        MaxAge:   _max_age,         // 消息保留24小时
    })
    if err != nil {
        zap.L().Panic("Stream info/create error:", zap.String("err", err.Error()))
    }
    return &NatsBroker{NC: nc, JS: js}, nil
}
// Close 关闭NATS连接
// 使用Drain方法优雅关闭,处理完本地缓存的消息后再断开连接
func (b *NatsBroker) Close() {
    if b.NC != nil {
        b.NC.Drain() // 优雅关闭:处理完本地缓存再断开
    }
}

2. 发布/消费消息

/*
generated by comer,https://github.com/imoowi/comer
Copyright © 2023 jun<simpleyuan@gmail.com>
*/
package services
import (
    "codee_jun/internal/components"
    "codee_jun/internal/interfaces"
    "codee_jun/internal/models"
    "codee_jun/internal/repos"
    "fmt"
    "runtime"
    "time"
    "github.com/gin-gonic/gin"
    "github.com/nats-io/nats.go"
    "github.com/spf13/cast"
    "go.uber.org/zap"
)
var Order *OrderService
// OrderService 订单服务
// 封装了订单的增删改查功能,并集成了NATS消息队列进行异步处理
type OrderService struct {
    interfaces.Service[*models.Order]                        // 基础服务接口,提供CRUD操作
    nb                                *components.NatsBroker // NATS消息队列代理,用于发布订阅消息
}
// NewOrderService 创建订单服务实例
// 初始化NATS连接,并注入订单仓储层
// 返回初始化完成的OrderService实例
func NewOrderService(r *repos.OrderRepo) *OrderService {
    nb, err := components.NewNatsBroker()
    if err != nil {
        panic(err)
    }
    // defer nb.NC.Close()
    _odS := &OrderService{
        Service: *interfaces.NewService(repos.Order),
        nb:      nb,
    }
    go _odS.JsWorker()
    return _odS
}
func init() {
    RegisterServices(func() {
        Order = NewOrderService(repos.Order)
    })
}
// Add 添加订单
// 在数据库中创建订单记录,成功后异步发布订单创建事件到NATS
// 参数 c: gin上下文,order: 订单数据
// 返回新订单ID和可能的错误
func (s *OrderService) Add(c *gin.Context, order *models.Order) (newId uint, err error) {
    id, err := s.Service.Add(c, order)
    if err == nil && id > 0 {
        go s.PublishJs(order)
    }
    return id, nil
}
// PublishJs 发布订单创建事件到NATS JetStream
// 将订单ID发布到"orders.created"主题,供其他服务订阅处理
// 参数 order: 订单数据
func (s *OrderService) PublishJs(order *models.Order) {
    _subject := "orders.created"
    _data := []byte(cast.ToString(order.ID))
    s.nb.JS.Publish(_subject, _data)
}
// JsWorker 订单事件消费者
// 订阅"orders.created"主题,处理订单创建事件
// 实现了消息确认和重试机制:
// - 消息处理成功时调用Ack确认
// - 消息处理失败时,根据重试次数决定是否延迟重试或终止消息
// - 最多重试5次,重试延迟采用指数退避策略(n²秒)
// 该方法会阻塞当前goroutine,需要在独立的goroutine中运行
func (s *OrderService) JsWorker() {
    _subject := "orders.created"
    _, err := s.nb.JS.QueueSubscribe(_subject, "order_worker"func(msg *nats.Msg) {
        orderId := string(msg.Data)
        zap.L().Info("order.worker", zap.String("orderId", orderId))
        err := s.processOrderCreatedEvent()
        if err != nil {
            zap.L().Error("order.worker", zap.Error(err))
            meta, _ := msg.Metadata()
            numdDelivered := meta.NumDelivered
            if numdDelivered < 3 {
                delay := time.Duration(numdDelivered*numdDelivered) * time.Second
                msg.NakWithDelay(delay)
            } else {
                msg.Term()
            }
            return
        }
        msg.Ack()
    }, nats.ManualAck(), nats.MaxDeliver(5))
    if err != nil {
        panic(err)
    }
    runtime.Goexit()
}
// processOrderCreatedEvent 处理订单创建事件
// 模拟订单事件处理逻辑,偶数时间戳会返回错误用于测试重试机制
// 返回处理过程中的错误
func (s *OrderService) processOrderCreatedEvent() error {
    if time.Now().Unix()%2 == 0 {
        return fmt.Errorf("order created event is even")
    }
    return nil
}

3. 运行测试

//生成swagger
swag init  --parseDependency=true

//启动服务 
go run . server -c configs/config.yaml 

浏览器访问地址:

http://localhost:8080/swagger/index.html#/order/post\\_api\\_orders

图片

新增一个订单

curl -X 'POST' \
  'http://localhost:8080/api/orders' \
  -H 'accept: application/json' \
  -H 'Authorization: 1' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Codee君"
}'
24

在消费者就收到了订单创建的消息

图片

文末升华

HTTP 是面对面交流,
Kafka 是档案室,
而 NATS,
是让系统彼此“活着”的那层空气。

*源码地址*

1、公众号“Codee君”回复“每日一Go”获取源码

2、pan.baidu.com/s/1B6pgLWfS…

友情链接:加班费计算器(vx小程序搜索“加班计”)

原文地址 mp.weixin.qq.com/s/au93KJBM5…


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!