前言
你好,欢迎来到这份零基础 RabbitMQ 教程!
如果你是第一次听说消息队列,或者听说过但一直不知道怎么上手,这份教程就是为你准备的。我们会用最通俗的语言,从什么是RabbitMQ开始,一步步带你掌握它的核心用法,并且全程使用 Go 语言来做代码演示,让你看完就能动手跑起来。
RabbitMQ 是目前最流行的开源消息中间件之一,它基于 AMQP 协议(Advanced Message Queuing Protocol),能够帮我们实现系统解耦、异步通信、流量削峰等功能,是微服务架构中必不可少的组件。
读完这份教程,你将学会:
-
RabbitMQ的核心概念,再也不怕听不懂术语 -
如何在各种系统上快速安装部署
RabbitMQ -
如何用 Go 语言连接和操作
RabbitMQ -
RabbitMQ的 6 种核心工作模式,以及它们的适用场景 -
生产环境必备的高级特性:消息确认、持久化、死信队列等
一、RabbitMQ 核心概念:用快递站帮你搞懂
很多新手刚接触 RabbitMQ 时,会被一堆术语搞晕:生产者、消费者、交换机、队列… 别担心,我们用生活中最常见的快递站来类比,保证你一看就懂。
1.1 核心组件类比
想象一下,你开了一家淘宝店,每天要给全国各地的用户发快递。你不会自己一个个送快递,而是把包裹交给快递站,由快递站来帮你完成后续的配送。
RabbitMQ 就相当于这个 快递站,它的各个组件对应快递站的不同角色:
RabbitMQ 术语 | 快递站类比 | 作用说明 |
|---|---|---|
| 生产者 (Producer) | 发快递的你(淘宝卖家) | 负责生产消息,把消息发送给 RabbitMQ |
| 消费者 (Consumer) | 收快递的买家 | 负责从 RabbitMQ 接收并处理消息 |
| 交换机 (Exchange) | 快递分拣中心 | 接收你发来的包裹,根据地址把包裹分到不同的快递柜 |
| 队列 (Queue) | 小区的快递柜 | 用来暂存包裹,买家什么时候有空什么时候来取 |
| 绑定 (Binding) | 分拣中心到快递柜的路线 | 告诉分拣中心:XX 地址的包裹,要放到 XX 快递柜里 |
| 路由键 (Routing Key) | 包裹上的收货地址 | 你发包裹时写的地址,用来告诉分拣中心这个包裹要送到哪(也就是目的地) |
| 虚拟主机 (VHost) | 独立的快递园区 | 用来隔离不同的业务,比如淘宝的快递和京东的快递各用一个园区,互不干扰 |
1.2 消息流转的完整流程
搞懂了这些角色,整个消息流转的过程就非常清晰了:
-
你(生产者)打包好商品,写好收货地址(路由键),把包裹交给快递站(
RabbitMQ) -
包裹先送到分拣中心(交换机)
-
分拣中心根据你写的地址,按照之前定好的路线(绑定),把包裹放到对应的快递柜(队列)里
-
买家(消费者)下班后来到小区,从对应的快递柜里取出包裹,完成签收
整个过程中:
-
你不需要等买家签收,交完包裹就可以去忙别的了(异步处理)
-
就算买家今天没空取,包裹也会一直存在快递柜里,等他明天来取(消息存储)
-
双 11 包裹爆仓时,快递柜能帮你暂存所有包裹,不会因为突然的流量把系统冲垮(流量削峰)
1.3 架构图直观理解
下面这张架构图,能帮你更直观地理解这些组件之间的关系:

二、环境搭建:5 分钟搞定 RabbitMQ 服务
接下来我们要把 RabbitMQ 服务跑起来,这里我最推荐你用 Docker 来安装,因为它最简单,一行命令就能搞定,不用管什么依赖、版本问题。当然,我也会给你提供其他系统的安装方式。
2.1 推荐方式:Docker 一键安装
如果你已经装了 Docker,只需要执行下面这一条命令:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management

第一次运行会自动下载镜像,等它下载完了,容器就会启动成功了。我这里已经是下载好了,所以直接就启动了。
解释一下这几个参数:
-
-p 5672:5672:这是 RabbitMQ 的通信端口,我们的 Go 程序就是通过这个端口连接 RabbitMQ 的 -
-p 15672:15672:这是 Web 管理界面的端口,装完之后我们可以通过浏览器来可视化管理 RabbitMQ -
rabbitmq:3-management:这个镜像已经内置了管理插件,不用我们手动装了
执行完之后,等个几十秒,容器启动成功就好了。
2.2 其他系统的安装方式
如果你不想用 Docker,也可以用系统包管理器安装:
Ubuntu/Debian
# 安装 Erlang 环境(RabbitMQ 是用 Erlang 写的)
sudo apt-get update
sudo apt-get install erlang
# 安装 RabbitMQ
sudo apt-get install rabbitmq-server
# 启动服务
sudo systemctl start rabbitmq-server
# 启用管理插件
sudo rabbitmq-plugins enable rabbitmq_management
CentOS/RHEL
# 添加 yum 源
sudo vim /etc/yum.repos.d/rabbitmq.repo
# 写入以下内容
[rabbitmq_server]
name=rabbitmq_server
baseurl=https://packagecloud.io/rabbitmq/rabbitmq-server/el/7/$basearch
repo_gpgcheck=1
gpgcheck=1
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
# 安装
sudo yum install -y rabbitmq-server
# 启动服务
sudo systemctl start rabbitmq-server
# 启用管理插件
sudo rabbitmq-plugins enable rabbitmq_management
Windows
直接去官网下载安装包:https://www.rabbitmq.com/download.html
下载完之后一路下一步安装就行,安装完成后服务会自动启动。
2.3 访问 Web 管理界面
不管你用哪种方式安装,装完之后,打开浏览器,访问:
http://localhost:15672
你会看到这样的登录界面:

-
默认用户名:
guest -
默认密码:
guest
登录进去之后,你就能看到 RabbitMQ 的管理后台了,长这样:

这个后台非常有用,你可以在这里查看队列、交换机、连接数,还能手动发消息、查看消息堆积情况,后面我们跑代码的时候,也可以在这里实时看到消息的流转。
注意:默认的
guest用户只能从localhost访问,如果你的 RabbitMQ 是部署在远程服务器上,需要新建一个管理员用户,我会在后面的最佳实践部分讲。
2.4 Go 客户端安装
RabbitMQ 服务装好了,接下来我们要准备 Go 语言的客户端,用来连接和操作 RabbitMQ。
官方推荐的 Go 客户端是 amqp091-go,这是原来的 streadway/amqp 的官方维护版本,现在已经完全替代了旧的库。
创建一个新的 Go 项目(空项目即可),然后执行下面的命令安装依赖:
# 初始化项目(如果是新项目)
go mod init rabbitmq-demo
# 安装 RabbitMQ 客户端
go get github.com/rabbitmq/amqp091-go

好了,到这里,我们的环境就全部准备好了!接下来我们就可以开始写代码,一个个体验 RabbitMQ 的工作模式了。
三、入门实战:Hello World 简单队列
我们从最简单的 简单队列 模式开始,这是 RabbitMQ 最基础的用法,就像我们平时发消息给一个人,一对一的通信。
3.1 模式说明
简单队列的结构非常简单:
生产者 -> 队列 -> 消费者
-
生产者把消息发送到一个队列
-
消费者从这个队列里读取消息
-
一条消息只能被一个消费者消费一次
这个模式非常适合简单的点对点通信,比如:下单成功后,通知库存服务扣库存。
3.2 编写生产者代码
首先,我们写一个生产者,用来发送消息:
// producer.go
package main
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
// 错误处理的辅助函数
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// 1. 连接到 RabbitMQ 服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close() // 函数结束时关闭连接
// 2. 创建一个通道(Channel),所有的操作都是通过通道完成的
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 3. 声明一个队列,队列的名字叫 "hello"
q, err := ch.QueueDeclare(
"hello", // 队列名称
false, // 是否持久化(重启后队列是否保留)
false, // 是否自动删除(没有消费者时自动删除队列)
false, // 是否排他(只有当前连接能访问)
false, // 是否不等待
nil, // 额外参数
)
failOnError(err, "无法声明队列")
// 4. 要发送的消息
body := "Hello World! 这是我的第一条 RabbitMQ 消息"
// 5. 发送消息
err = ch.Publish(
"", // 交换机名称,这里用默认的交换机
q.Name, // 路由键,这里就是队列的名字
false, // 是否强制
false, // 是否立即
amqp.Publishing{
// ContentType 是消息的内容类型,这里我们用 text/plain 表示纯文本
ContentType: "text/plain", // 消息的内容类型
Body: []byte(body), // 消息的内容,转成字节数组
})
failOnError(err, "无法发送消息")
log.Printf(" [x] 已发送消息: %s", body)
}
3.3 编写消费者代码
然后,我们写一个消费者,用来接收消息:
// consumer.go
package main
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// 1. 同样的,先连接 RabbitMQ,创建通道
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
// 2. 创建一个通道(Channel),所有的操作都是通过通道完成的
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 3. 同样声明队列,这里要和生产者的队列名一致
// 为什么要两边都声明?因为我们不知道生产者和消费者谁先启动
// 如果队列已经存在,声明操作不会做任何事,只会返回队列的信息;如果队列不存在,声明操作会创建一个新的队列,所以很安全
q, err := ch.QueueDeclare(
"hello", // 队列名称
false, // 是否持久化(重启后队列是否保留)
false, // 是否自动删除(没有消费者时自动删除队列)
false, // 是否排他(只有当前连接能访问)
false, // 是否不等待
nil, // 额外参数
)
failOnError(err, "无法声明队列")
// 4. 注册消费者,监听这个队列的消息
msgs, err := ch.Consume(
q.Name, // 要监听的队列名称
"", // 消费者名称,空的话服务器会自动生成
true, // 是否自动确认(后面会详细讲)
false, // 是否排他(只有当前连接能访问)
false, // 是否本地
false, // 是否不等待
nil, // 额外参数
)
failOnError(err, "无法注册消费者")
// 5. 启动一个协程来处理消息
// 因为接收消息是阻塞的,所以我们要放到协程里,不阻塞主程序
go func() {
for d := range msgs {
// 收到消息了!
log.Printf(" [x] 收到消息: %s", d.Body)
}
}()
log.Printf(" [*] 等待消息中... 按 CTRL+C 退出")
// 阻塞主程序,不让它退出
<-make(chan bool)
}
3.4 运行测试
现在我们来运行一下:
- 首先打开一个终端,运行生产者:
go run producer.go
生产者这边会输出: [x] 已发送消息: Hello World! 这是我的第一条 RabbitMQ 消息

在生产者终端看到这个输出,说明消息已经成功发送到 RabbitMQ 了。
同时我们可以在 Web 管理后台的 Queues 标签页里看到 hello 队列,点击进去可以看到消息数量是 1,说明消息已经进入队列了。



在下方的 Get messages 标签页里,我们也能看到这条消息的内容:

- 再打开另一个终端,运行消费者:
go run consumer.go
你会看到输出: [*] 等待消息中... 按 CTRL+C 退出,说明消费者已经启动,在等消息了。
然后会再输出: [x] 收到消息: Hello World! 这是我的第一条 RabbitMQ 消息,说明消费者成功接收到了消息。

我们也可以在 Web 管理后台看到,hello 队列里的消息数量从 1 变成了 0,说明消息已经被消费者取走了。

完美!我们的第一个 RabbitMQ 程序就跑通了!
四、工作队列:任务分发与负载均衡
接下来我们看第二个模式:工作队列(Work Queue)。
4.1 模式说明
工作队列的结构是这样的:
生产者 -> 队列 -> 多个消费者
它的作用是什么呢?当我们有很多耗时的任务要处理时,我们可以启动多个消费者,让它们一起分担这些任务,实现负载均衡。
比如,我们有 10 个图片压缩的任务,启动两个消费者,每个消费者处理 5 个,这样处理速度就快了一倍。
4.2 轮询分发 vs 公平分发
RabbitMQ 默认的分发方式是轮询分发:它会把任务按顺序一个一个分给消费者,不管这个消费者有没有处理完上一个任务。
这就会有个问题:如果有一个任务特别耗时,另一个很快,就会导致一个消费者忙死,另一个闲死。
所以我们一般会用公平分发:告诉 RabbitMQ,一次只给一个消费者发一个任务,等这个消费者处理完了,再给它发下一个。这样就能保证任务会均匀地分给空闲的消费者。
4.3 代码实现
我们来写个例子,模拟任务处理:
生产者:发送任务
// task_producer.go
package main
import (
"log"
"strconv"
"strings"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 声明队列,这次我们把队列设为持久化
q, err := ch.QueueDeclare(
"task_queue", // 队列名
true, // 持久化,RabbitMQ 重启后队列不丢
false, // 不自动删除
false, // 不排他
false, // 不等待
nil,
)
failOnError(err, "无法声明队列")
// 发送10个任务
for i := 0; i < 10; i++ {
// 任务内容,我们用点来表示任务的耗时,一个点代表1秒
// 比如 "task3 ..." 表示这个任务要处理3秒
// strconv.Itoa(i):把整数 i 转换成字符串,这样我们就能看到每个任务的编号了
// strings.Repeat(".", i/2):重复 i/2 次点号,这样任务的耗时就会随着 i 的增加而增加
task := "task" + strconv.Itoa(i) + " " + strings.Repeat(".", i/2)
err = ch.Publish(
"", // 默认交换机
q.Name, // 路由键
false,
false,
amqp.Publishing{
// 消息也设为持久化,这样重启后消息不丢
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte(task),
})
failOnError(err, "无法发送消息")
log.Printf(" [x] 发送任务: %s", task)
time.Sleep(100 * time.Millisecond)
}
}
消费者:处理任务
// worker.go
package main
import (
"bytes"
"log"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
q, err := ch.QueueDeclare(
"task_queue", true, false, false, false, nil,
)
failOnError(err, "无法声明队列")
// 关键!设置预取计数,开启公平分发
// Qos 是 Quality of Service 的缩写,意思是服务质量,这个设置告诉 RabbitMQ 在同一时间只给一个消费者分发一个消息,直到这个消费者处理完了并确认了这个消息,RabbitMQ 才会给它分发下一个消息。这样就能实现公平分发了,不会让某个消费者因为处理慢而积压大量消息。
// 告诉 RabbitMQ,一次只给我一个任务,我处理完了你再给我下一个
err = ch.Qos(
1, // 预取数量
0, // 预取大小
false, // 是否全局
)
failOnError(err, "无法设置 Qos")
// 注册消费者,注意这里我们把自动确认关了,这样我们就可以在处理完消息后手动确认。
msgs, err := ch.Consume(
q.Name, "",
false, // 自动确认设为 false,我们要手动确认
false, false, false, nil,
)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
log.Printf(" [x] 开始处理任务: %s", d.Body)
// 模拟任务处理,点的数量就是耗时
t := time.Duration(bytes.Count(d.Body, []byte("."))) * time.Second
time.Sleep(t)
log.Printf(" [x] 任务处理完成: %s", d.Body)
// 手动确认消息,告诉 RabbitMQ:这个消息我处理完了,你可以删了
d.Ack(false)
}
}()
log.Printf(" [*] 工作进程已启动,等待任务...")
<-make(chan bool)
}
4.4 运行测试
现在我们来测试一下:
- 1个工作进程 : 1个生产者
先启动工作进程:
go run worker.go
你会看到输出: [*] 工作进程已启动,等待任务...,说明消费者已经启动了。
然后启动生产者:
go run task_producer.go

你会看到,生产者发送了 10 个任务,工作进程一个人吃了 10 个任务,处理完一个才处理下一个。
- 2个工作进程 : 1个生产者
启动两个工作进程,打开两个终端,都运行:
go run worker.go
然后运行生产者:
go run task_producer.go

你会看到,两个工作进程会自动分担这 10 个任务,快的消费者会处理更多的任务,慢的也不会被拖累,这就是公平分发的效果。
这里要重点注意:我们把自动确认关了,用了手动确认。这非常重要!如果消费者处理到一半崩溃了,手动确认的话,RabbitMQ 会把这个任务重新放回队列,交给另一个消费者处理,不会丢消息。
五、发布订阅:广播消息给所有消费者
接下来是 发布 / 订阅(Publish/Subscribe) 模式,这个模式下,一条消息会被发送给所有的消费者,也就是广播。
5.1 模式说明
发布订阅的结构是这样的:
生产者 -> 交换机(Fanout) -> 多个队列 -> 多个消费者
这里我们用到了 Fanout 类型的交换机,这种交换机的作用很简单:它会把收到的所有消息,广播给所有绑定到它的队列,完全不管路由键是什么。
比如,我们做一个日志系统,所有服务的日志都发给这个交换机,然后:
-
一个消费者把日志存到磁盘
-
一个消费者把日志打印到屏幕
-
一个消费者把日志发到监控系统
所有消费者都能收到同一条日志消息,互不干扰。
5.2 代码实现
生产者:发送日志
// log_producer.go
package main
import (
"context"
"log"
"strconv"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 声明一个 Fanout 类型的交换机
err = ch.ExchangeDeclare(
"logs", // 交换机名称
"fanout", // 类型:fanout 就是广播
true, // 持久化
false, // 自动删除
false, // 内部
false, // 不等待
nil,
)
failOnError(err, "无法声明交换机")
// 每隔1秒发一条日志
for i := 0; ; i++ {
logMsg := "这是一条日志消息,序号: " + strconv.Itoa(i)
// 发送消息到交换机
err = ch.PublishWithContext(
context.Background(),
"logs", // 交换机名称
"", // 路由键,fanout 模式下这个参数会被忽略,随便写什么都行
false, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(logMsg),
})
failOnError(err, "无法发送消息")
log.Printf(" [x] 发送日志: %s", logMsg)
time.Sleep(1 * time.Second)
}
}
消费者 1:打印日志到屏幕
// log_console.go
package main
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 同样声明交换机,要和生产者的一致,否则消费者无法接收到消息
err = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
failOnError(err, "无法声明交换机")
// 而且我们把 exclusive 设为 true,这样消费者断开连接后,队列会自动删除
q, err := ch.QueueDeclare(
"log_console_queue", // 队列名
false, // 不持久化
false, // 不自动删除
true, // 排他,连接断了就删
false, // 不等待
nil,
)
failOnError(err, "无法声明队列")
// 把队列绑定到交换机上
err = ch.QueueBind(
q.Name, // 队列名
"", // 路由键,fanout 模式下忽略
"logs", // 交换机名
false,
nil,
)
failOnError(err, "无法绑定队列")
// 消费消息
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
log.Printf(" [控制台消费者] 收到日志: %s", d.Body)
}
}()
log.Println(" [*] 控制台日志消费者已启动")
<-make(chan bool)
}
消费者 2:把日志存到文件
// log_file.go
package main
import (
"log"
"os"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
err = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
failOnError(err, "无法声明交换机")
q, err := ch.QueueDeclare("log_file_queue", false, false, true, false, nil)
failOnError(err, "无法声明队列")
err = ch.QueueBind(q.Name, "", "logs", false, nil)
failOnError(err, "无法绑定队列")
// 打开文件,准备写日志
f, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer f.Close()
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
logMsg := string(d.Body)
log.Printf(" [文件消费者] 保存日志到文件: %s", logMsg)
// 写入文件
f.WriteString(logMsg + "\n")
f.Sync()
}
}()
log.Println(" [*] 文件日志消费者已启动")
<-make(chan bool)
}
5.3 运行测试
现在运行一下:
- 先启动控制台消费者:
go run log_console.go
- 再启动文件消费者:
go run log_file.go
- 最后启动生产者:
go run log_producer.go
你会发现,两个消费者都能收到所有的日志消息!生产者发一条,两个消费者各收到一条,完美实现了广播的效果。

六、路由模式:精准定向分发消息
接下来是路由模式(Routing),这个模式下,我们可以根据消息的类型,把消息发给指定的消费者。
6.1 模式说明
路由模式用的是 Direct 类型的交换机,这种交换机会根据路由键的精确匹配来转发消息。
比如,我们还是做日志系统,这次我们想:
-
error级别的日志,存到磁盘,用来排查问题 -
info、warning级别的日志,只打印到屏幕
那我们就可以:
-
生产者发消息时,给 error 日志的路由键设为
error,info 的设为info,warning 的设为warning -
存磁盘的消费者,只绑定
error这个路由键 -
打印屏幕的消费者,绑定
info和warning两个路由键
这样,error 日志两个消费者都能收到,info 日志只有屏幕消费者能收到,非常灵活。
6.2 代码实现
生产者:按级别发日志
// routing_producer.go
package main
import (
"context"
"log"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 声明 Direct 类型的交换机
err = ch.ExchangeDeclare(
"logs_direct", // 交换机名
"direct", // 类型:direct
true, false, false, false, nil,
)
failOnError(err, "无法声明交换机")
// 模拟发不同级别的日志
logs := []struct {
level string
message string
}{
{"info", "这是一条普通的信息日志"},
{"warning", "这是一条警告日志"},
{"error", "这是一条错误日志!系统出问题了"},
}
for _, l := range logs {
err = ch.PublishWithContext(
context.Background(),
"logs_direct", // 交换机
l.level, // 路由键,就是日志级别
false, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(l.message),
})
failOnError(err, "无法发送消息")
log.Printf(" [x] 发送[%s]日志: %s", l.level, l.message)
time.Sleep(500 * time.Millisecond)
}
}
消费者 1:只接收 error 日志,存到磁盘
// error_consumer.go
package main
import (
"log"
"os"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
err = ch.ExchangeDeclare("logs_direct", "direct", true, false, false, false, nil)
failOnError(err, "无法声明交换机")
q, err := ch.QueueDeclare("", false, false, true, false, nil)
failOnError(err, "无法声明队列")
// 只绑定 error 路由键
err = ch.QueueBind(q.Name, "error", "logs_direct", false, nil)
failOnError(err, "无法绑定队列")
f, err := os.OpenFile("error.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer f.Close()
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
logMsg := string(d.Body)
log.Printf(" [Error消费者] 保存错误日志: %s", logMsg)
f.WriteString(logMsg + "\n")
f.Sync()
}
}()
log.Println(" [*] Error 消费者已启动,只处理 error 日志")
<-make(chan bool)
}
消费者 2:接收所有日志,打印到屏幕
// all_log_consumer.go
package main
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
err = ch.ExchangeDeclare("logs_direct", "direct", true, false, false, false, nil)
failOnError(err, "无法声明交换机")
q, err := ch.QueueDeclare("", false, false, true, false, nil)
failOnError(err, "无法声明队列")
// 绑定 info 路由键
err = ch.QueueBind(q.Name, "info", "logs_direct", false, nil)
failOnError(err, "无法绑定队列")
// 绑定 warning 路由键
err = ch.QueueBind(q.Name, "warning", "logs_direct", false, nil)
failOnError(err, "无法绑定队列")
// 绑定 error 路由键
err = ch.QueueBind(q.Name, "error", "logs_direct", false, nil)
failOnError(err, "无法绑定队列")
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
log.Printf(" [控制台消费者] 收到日志: %s", d.Body)
}
}()
log.Println(" [*] 控制台消费者已启动,处理所有级别日志")
<-make(chan bool)
}
6.3 运行测试
运行一下:
- 启动 error 消费者:
go run error_consumer.go
- 启动控制台消费者:
go run all_log_consumer.go
- 启动生产者:
go run routing_producer.go
你会看到:

-
error日志,两个消费者都收到了 -
info和warning日志,只有控制台消费者收到了
完美!这就是精准路由的效果。
七、主题模式:通配符灵活路由
接下来是最灵活的主题模式(Topic),这个模式下,路由键支持通配符,我们可以做更复杂的消息过滤。
7.1 模式说明
主题模式用的是 Topic 类型的交换机,它和 Direct 很像,但是它的路由键匹配支持通配符:
-
*:匹配一个单词 -
#:匹配零个或多个单词
这里的单词,是用点 . 分隔的。
比如,我们的路由键可以是 order.create、order.update、user.register 这种格式,然后:
-
order.*:可以匹配order.create、order.update,但是匹配不了user.register -
#:可以匹配所有的路由键 -
*.create:可以匹配order.create、user.create
这个模式非常适合多层级的消息路由,比如我们要按模块、按操作类型来过滤消息。
7.2 代码实现
我们来做一个订单和用户的消息通知系统:
生产者:发送不同类型的消息
// topic_producer.go
package main
import (
"context"
"log"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 声明 Topic 类型的交换机
err = ch.ExchangeDeclare(
"events_topic", // 交换机名
"topic", // 类型:topic
true, false, false, false, nil,
)
failOnError(err, "无法声明交换机")
// 模拟发送不同的事件
events := []struct {
routingKey string
message string
}{
{"order.create", "用户创建了新订单"},
{"order.update", "订单状态更新了"},
{"user.register", "新用户注册了"},
{"user.login", "用户登录了"},
}
for _, e := range events {
// Publish只能发送消息到交换机,路由键由我们指定,消费者根据路由键来决定是否接收消息
// 而PublishWithContext可以指定上下文,支持超时等功能
err = ch.PublishWithContext(
context.Background(), // 上下文:这里使用背景上下文,表示没有特定的超时或取消条件
"events_topic", // 交换机名
e.routingKey, // 路由键,消费者根据这个键来决定是否接收消息
false, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(e.message),
})
failOnError(err, "无法发送消息")
log.Printf(" [x] 发送事件[%s]: %s", e.routingKey, e.message)
time.Sleep(500 * time.Millisecond)
}
}
消费者 1:只关心订单相关的事件
// order_consumer.go
package main
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
err = ch.ExchangeDeclare("events_topic", "topic", true, false, false, false, nil)
failOnError(err, "无法声明交换机")
q, err := ch.QueueDeclare("", false, false, true, false, nil)
failOnError(err, "无法声明队列")
// 绑定 order.*,匹配所有 order 开头的事件
err = ch.QueueBind(q.Name, "order.*", "events_topic", false, nil)
failOnError(err, "无法绑定队列")
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
log.Printf(" [订单服务] 收到事件: %s", d.Body)
}
}()
log.Println(" [*] 订单服务消费者已启动,只处理订单相关事件")
<-make(chan bool)
}
消费者 2:关心所有的事件
// all_event_consumer.go
package main
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
err = ch.ExchangeDeclare("events_topic", "topic", true, false, false, false, nil)
failOnError(err, "无法声明交换机")
q, err := ch.QueueDeclare("", false, false, true, false, nil)
failOnError(err, "无法声明队列")
// 绑定 #,匹配所有的事件
err = ch.QueueBind(q.Name, "#", "events_topic", false, nil)
failOnError(err, "无法绑定队列")
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
failOnError(err, "无法注册消费者")
go func() {
for d := range msgs {
log.Printf(" [监控服务] 收到所有事件: %s", d.Body)
}
}()
log.Println(" [*] 监控服务消费者已启动,处理所有事件")
<-make(chan bool)
}
7.3 运行测试
运行一下:
- 启动订单消费者:
go run order_consumer.go
- 启动监控消费者:
go run all_event_consumer.go
- 启动生产者:
go run topic_producer.go
你会看到:

-
订单相关的两个事件,两个消费者都收到了
-
用户相关的两个事件,只有监控消费者收到了
这就是主题模式的灵活之处,你可以用通配符很方便地做各种消息过滤。
八、RPC 模式:同步远程调用
前面的模式都是异步的,但是有时候我们需要同步的调用:我发一个请求,然后等你给我返回结果,这就是 RPC(远程过程调用)模式。
8.1 模式说明
RPC 模式的流程是这样的:
-
客户端发送一个请求消息,同时指定两个东西:
-
reply_to:告诉服务端,处理完结果要把响应发到哪个队列 -
correlation_id:请求的唯一 ID,用来区分不同的请求
-
-
服务端收到请求,处理,然后把响应发到
reply_to指定的队列,并且带上correlation_id -
客户端监听响应队列,根据
correlation_id找到对应的响应
这样就实现了同步的远程调用。
8.2 代码实现
我们来实现一个计算斐波那契数的 RPC 服务:
RPC 服务端
// rpc_server.go
package main
import (
"log"
"strconv"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
// 计算斐波那契数
func fib(n int) int {
if n == 0 {
return 0
}
if n == 1 {
return 1
}
return fib(n-1) + fib(n-2)
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
defer ch.Close()
// 声明 RPC 请求队列
q, err := ch.QueueDeclare(
"rpc_queue", // 请求队列名
false, false, false, false, nil,
)
failOnError(err, "无法声明队列")
// 设置 Qos,保证公平分发
err = ch.Qos(1, 0, false)
failOnError(err, "无法设置 Qos")
msgs, err := ch.Consume(
q.Name, "", false, false, false, false, nil,
)
failOnError(err, "无法注册消费者")
log.Println(" [*] RPC 服务已启动,等待请求...")
for d := range msgs {
// 解析请求参数
n, err := strconv.Atoi(string(d.Body))
if err != nil {
log.Printf("无效的参数: %s", d.Body)
d.Ack(false)
continue
}
log.Printf(" [.] 计算 fib(%d)", n)
// 计算结果
response := fib(n)
// 把响应发回给客户端
err = ch.Publish(
"", // 默认交换机
d.ReplyTo, // 客户端指定的响应队列
false, false,
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: d.CorrelationId, // 带上请求的ID
Body: []byte(strconv.Itoa(response)),
})
failOnError(err, "无法发送响应")
// 确认请求消息
d.Ack(false)
}
}
RPC 客户端
// rpc_client.go
package main
import (
"log"
"math/rand"
"strconv"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
type RPCClient struct {
conn *amqp.Connection
channel *amqp.Channel
callbackQueue amqp.Queue
corrID string
responses map[string]chan string
}
func NewRPCClient() *RPCClient {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
ch, err := conn.Channel()
failOnError(err, "无法创建通道")
// 声明回调队列,用来接收响应
callbackQueue, err := ch.QueueDeclare(
"", false, false, true, false, nil,
)
failOnError(err, "无法声明回调队列")
// 监听回调队列
msgs, err := ch.Consume(
callbackQueue.Name, "", true, false, false, false, nil,
)
failOnError(err, "无法注册消费者")
client := &RPCClient{
conn: conn,
channel: ch,
callbackQueue: callbackQueue,
responses: make(map[string]chan string),
}
// 后台协程处理响应
go client.handleResponses(msgs)
return client
}
func (c *RPCClient) handleResponses(msgs <-chan amqp.Delivery) {
for d := range msgs {
// 根据 correlationId 找到对应的响应通道
corrId := d.CorrelationId
if ch, ok := c.responses[corrId]; ok {
ch <- string(d.Body)
}
}
}
func (c *RPCClient) Call(n int) (string, error) {
// 生成唯一的 correlationId
c.corrID = strconv.Itoa(rand.Intn(999999))
ch := make(chan string)
c.responses[c.corrID] = ch
// 发送请求
err := c.channel.Publish(
"", "rpc_queue", false, false,
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: c.corrID,
ReplyTo: c.callbackQueue.Name, // 告诉服务端响应发到哪
Body: []byte(strconv.Itoa(n)),
})
failOnError(err, "无法发送请求")
// 等待响应
response := <-ch
delete(c.responses, c.corrID)
return response, nil
}
func (c *RPCClient) Close() {
c.conn.Close()
}
func main() {
client := NewRPCClient()
defer client.Close()
// 调用 RPC,计算 fib(30)
log.Println(" [x] 请求 fib(30)")
res, err := client.Call(30)
failOnError(err, "RPC 调用失败")
log.Printf(" [.] 结果: %s", res)
}
8.3 运行测试
运行一下:
- 先启动服务端:
go run rpc_server.go
- 然后启动客户端:
go run rpc_client.go
客户端会输出:
[x] 请求 fib(30)
[.] 结果: 832040

我们就实现了一个基于 RabbitMQ 的 RPC 调用。
可以单独运行
rpc_client.go,然后去 Web 管理界面查看rpc_queue队列,你会看到客户端发出的请求消息,服务端处理完后,消息就会从队列里消失。

九、生产环境必备:高级特性
到这里,RabbitMQ 的核心工作模式我们就都讲完了,接下来我们讲几个生产环境必须要掌握的高级特性。
9.1 消息确认机制(ACK)
前面我们已经提到过,消息确认机制是保证消息不丢失的关键。
当你把 autoAck 设为 true 时,RabbitMQ 把消息发出去之后,就立刻把消息删了,不管消费者有没有处理完。如果消费者刚收到消息,还没处理就崩溃了,那这个消息就丢了。
而当你把 autoAck 设为 false 时,RabbitMQ 会等消费者手动调用 d.Ack() 之后,才会把消息删掉。如果消费者崩溃了,RabbitMQ 会把这个消息重新放回队列,发给其他消费者,保证消息不会丢。
除了 Ack,还有两个方法:
-
d.Nack(false, true):拒绝这个消息,并且让它重新入队,用来重试 -
d.Nack(false, false):拒绝这个消息,并且直接丢弃,用来处理坏消息
9.2 消息持久化
持久化是为了保证 RabbitMQ 重启之后,消息不会丢。要实现完整的持久化,你需要同时设置三个地方:
-
队列的
durable参数设为true:这样RabbitMQ重启后,队列不会丢 -
消息的
DeliveryMode设为amqp.Persistent:这样消息会被存到磁盘,重启后不会丢 -
交换机的
durable参数设为true:这样交换机重启后不会丢
注意:这三个要同时设置,缺一不可!不然还是会丢消息。
9.3 发布确认(Publisher Confirm)
前面的 ACK 是消费者给 RabbitMQ 的确认,那生产者怎么知道消息真的被 RabbitMQ 收到了呢?这就需要发布确认机制。
开启发布确认后,RabbitMQ 收到消息后,会给生产者发一个确认,告诉生产者:我收到了。如果没收到,生产者就可以重试。
代码示例:
// 开启发布确认
err = ch.Confirm(false)
failOnError(err, "无法开启确认")
// 创建一个通道来接收确认
confirms := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
go func() {
for confirm := range confirms {
if confirm.Ack {
log.Printf("消息 %d 已被服务器确认", confirm.DeliveryTag)
} else {
log.Printf("消息 %d 被服务器拒绝了", confirm.DeliveryTag)
// 重试发送
}
}
}()
9.4 死信队列(DLQ)
死信队列,就是用来存放那些 死掉 的消息的队列。什么是死信?就是:
-
消息被消费者拒绝了,并且没有重新入队
-
消息过期了(
TTL到了) -
队列满了,消息放不下了
这些消息不会直接被扔掉,而是会被转发到死信交换机,然后存到死信队列里,我们可以后续排查问题,或者重试处理。
最常见的用法就是用死信队列实现延迟消息:比如我们要做订单超时关闭,下单后 30 分钟没支付,就自动关闭订单。
我们可以这样做:
-
声明一个普通队列,设置它的
TTL是 30 分钟,并且设置死信交换机 -
下单的时候,把消息发到这个队列
-
30 分钟后,消息过期了,就会被转发到死信队列
-
我们的消费者监听死信队列,收到消息后,就去检查订单状态,如果没支付就关闭
代码示例:
// 声明死信交换机
err = ch.ExchangeDeclare("dlx_exchange", "direct", true, false, false, false, nil)
failOnError(err, "无法声明死信交换机")
// 声明死信队列
dlq, err := ch.QueueDeclare("dlq_queue", true, false, false, false, nil)
failOnError(err, "无法声明死信队列")
// 绑定死信队列到死信交换机
err = ch.QueueBind(dlq.Name, "order_timeout", "dlx_exchange", false, nil)
failOnError(err, "无法绑定死信队列")
// 声明延迟队列,设置 TTL 和死信交换机
args := amqp.Table{
"x-message-ttl": 30 * 60 * 1000, // 30分钟,单位毫秒
"x-dead-letter-exchange": "dlx_exchange", // 死信交换机
"x-dead-letter-routing-key": "order_timeout", // 死信路由键
}
delayQueue, err := ch.QueueDeclare("delay_queue", true, false, false, false, args)
failOnError(err, "无法声明延迟队列")
这样,我们把订单消息发到 delay_queue,30 分钟后,它就会自动跑到 dlq_queue 里,我们的消费者就可以处理了。
十、最佳实践与常见问题
最后,给你一些生产环境的最佳实践:
10.1 用户权限管理
默认的 guest 用户只能本地访问,生产环境一定要新建用户,不要用默认的:
# 添加用户
rabbitmqctl add_user myuser mypassword
# 设置用户为管理员
rabbitmqctl set_user_tags myuser administrator
# 给用户授权
rabbitmqctl set_permissions -p / myuser ".*" ".*" ".*"
10.2 连接与重连
RabbitMQ 的连接是会断的,网络抖动、服务重启都会导致连接断开,所以实际开发时,你的客户端一定要有重连机制,不要断了就直接挂了。
可以把连接的代码放在一个循环里,连接失败了就等几秒钟再重试:
for {
conn, err := amqp.Dial("amqp://myuser:mypassword@localhost:5672/")
if err != nil {
log.Printf("连接失败,5秒后重试: %s", err)
time.Sleep(5 * time.Second)
continue
}
// 连接成功,继续后续操作
break
}
如果连接后发生网络抖动,导致连接断开了,也要监听连接的 NotifyClose 通道,及时重连:
conn, err := amqp.Dial("amqp://myuser:mypassword@localhost:5672/")
failOnError(err, "无法连接到 RabbitMQ")
// 监听连接关闭事件
notifyClose := conn.NotifyClose(make(chan *amqp.Error, 1))
go func() {
err := <-notifyClose
log.Printf("连接关闭: %s", err)
// 重连逻辑
for {
conn, err = amqp.Dial("amqp://myuser:mypassword@localhost:5672/")
if err != nil {
log.Printf("重连失败,5秒后重试: %s", err)
time.Sleep(5 * time.Second)
continue
}
log.Println("重连成功")
break
}
}()
10.3 幂等性消费
网络问题可能会导致消息重复投递,所以你的消费端一定要保证幂等性:就是同一个消息,消费多少次,结果都是一样的。
常见的做法:
-
用消息的唯一 ID,存到
Redis,消费之前先查一下有没有处理过 -
数据库的唯一索引,防止重复插入
-
业务状态机,比如订单状态,只有待支付的才能处理超时,已经支付的就忽略
10.4 监控
一定要开启监控,监控队列的堆积情况、连接数、消息速率,有问题及时报警。RabbitMQ 的管理后台已经提供了很多指标,你也可以用 Prometheus 来采集。
总结
恭喜你!看完这份教程,你已经掌握了 RabbitMQ 的核心用法了!
我们从最基础的概念开始,一步步学会了:
-
RabbitMQ的核心组件和消息流转 -
6 种核心工作模式:简单队列、工作队列、发布订阅、路由、主题、RPC
-
生产环境必备的高级特性:ACK、持久化、死信队列
-
各种场景下的 Go 代码实现
RabbitMQ 是一个非常强大的工具,掌握了它,你就能在分布式系统中轻松实现异步通信、解耦、削峰这些功能。
接下来,你可以自己动手把这些代码跑一遍,加深理解,然后就可以在你的项目中使用它了!
&spm=1001.2101.3001.5002&articleId=159656861&d=1&t=3&u=3e02ec0e44bf490fa781e4848f5830b3)

被折叠的 条评论
为什么被折叠?



