一、什么是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
| 对比项 | NATS | Kafka | RabbitMQ |
| 延迟 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 吞吐 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 运维复杂度 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 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”获取源码
友情链接:加班费计算器(vx小程序搜索“加班计”)
原文地址 mp.weixin.qq.com/s/au93KJBM5…
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!