消息队列:RabbitMQ零基础教程(Go版)

前言

你好,欢迎来到这份零基础 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 消息流转的完整流程

搞懂了这些角色,整个消息流转的过程就非常清晰了:

  1. 你(生产者)打包好商品,写好收货地址(路由键),把包裹交给快递站(RabbitMQ

  2. 包裹先送到分拣中心(交换机)

  3. 分拣中心根据你写的地址,按照之前定好的路线(绑定),把包裹放到对应的快递柜(队列)里

  4. 买家(消费者)下班后来到小区,从对应的快递柜里取出包裹,完成签收

整个过程中:

  • 你不需要等买家签收,交完包裹就可以去忙别的了(异步处理)

  • 就算买家今天没空取,包裹也会一直存在快递柜里,等他明天来取(消息存储)

  • 双 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 运行测试

现在我们来运行一下:

  1. 首先打开一个终端,运行生产者:
go run producer.go

生产者这边会输出: [x] 已发送消息: Hello World! 这是我的第一条 RabbitMQ 消息

在这里插入图片描述

在生产者终端看到这个输出,说明消息已经成功发送到 RabbitMQ 了。

同时我们可以在 Web 管理后台的 Queues 标签页里看到 hello 队列,点击进去可以看到消息数量是 1,说明消息已经进入队列了。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

  1. 再打开另一个终端,运行消费者:
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个工作进程 : 1个生产者
    先启动工作进程:
go run worker.go

你会看到输出: [*] 工作进程已启动,等待任务...,说明消费者已经启动了。

然后启动生产者:

go run task_producer.go

在这里插入图片描述

你会看到,生产者发送了 10 个任务,工作进程一个人吃了 10 个任务,处理完一个才处理下一个。

  1. 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 运行测试

现在运行一下:

  1. 先启动控制台消费者:
go run log_console.go
  1. 再启动文件消费者:
go run log_file.go
  1. 最后启动生产者:
go run log_producer.go

你会发现,两个消费者都能收到所有的日志消息!生产者发一条,两个消费者各收到一条,完美实现了广播的效果。

在这里插入图片描述


六、路由模式:精准定向分发消息

接下来是路由模式(Routing),这个模式下,我们可以根据消息的类型,把消息发给指定的消费者。

6.1 模式说明

路由模式用的是 Direct 类型的交换机,这种交换机会根据路由键的精确匹配来转发消息。

比如,我们还是做日志系统,这次我们想:

  • error 级别的日志,存到磁盘,用来排查问题

  • infowarning 级别的日志,只打印到屏幕

那我们就可以:

  • 生产者发消息时,给 error 日志的路由键设为 error,info 的设为 info,warning 的设为 warning

  • 存磁盘的消费者,只绑定 error 这个路由键

  • 打印屏幕的消费者,绑定 infowarning 两个路由键

这样,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 运行测试

运行一下:

  1. 启动 error 消费者:
go run error_consumer.go
  1. 启动控制台消费者:
go run all_log_consumer.go
  1. 启动生产者:
go run routing_producer.go

你会看到:

在这里插入图片描述

  • error 日志,两个消费者都收到了

  • infowarning 日志,只有控制台消费者收到了

完美!这就是精准路由的效果。


七、主题模式:通配符灵活路由

接下来是最灵活的主题模式(Topic),这个模式下,路由键支持通配符,我们可以做更复杂的消息过滤。

7.1 模式说明

主题模式用的是 Topic 类型的交换机,它和 Direct 很像,但是它的路由键匹配支持通配符:

  • *:匹配一个单词

  • #:匹配零个或多个单词

这里的单词,是用点 . 分隔的。

比如,我们的路由键可以是 order.createorder.updateuser.register 这种格式,然后:

  • order.*:可以匹配 order.createorder.update,但是匹配不了 user.register

  • #:可以匹配所有的路由键

  • *.create:可以匹配 order.createuser.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 运行测试

运行一下:

  1. 启动订单消费者:
go run order_consumer.go
  1. 启动监控消费者:
go run all_event_consumer.go
  1. 启动生产者:
go run topic_producer.go

你会看到:

在这里插入图片描述

  • 订单相关的两个事件,两个消费者都收到了

  • 用户相关的两个事件,只有监控消费者收到了

这就是主题模式的灵活之处,你可以用通配符很方便地做各种消息过滤。


八、RPC 模式:同步远程调用

前面的模式都是异步的,但是有时候我们需要同步的调用:我发一个请求,然后等你给我返回结果,这就是 RPC(远程过程调用)模式。

8.1 模式说明

RPC 模式的流程是这样的:

  1. 客户端发送一个请求消息,同时指定两个东西:

    • reply_to:告诉服务端,处理完结果要把响应发到哪个队列

    • correlation_id:请求的唯一 ID,用来区分不同的请求

  2. 服务端收到请求,处理,然后把响应发到 reply_to 指定的队列,并且带上 correlation_id

  3. 客户端监听响应队列,根据 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 运行测试

运行一下:

  1. 先启动服务端:
go run rpc_server.go
  1. 然后启动客户端:
go run rpc_client.go

客户端会输出:

[x] 请求 fib(30)
[.] 结果: 832040

在这里插入图片描述

我们就实现了一个基于 RabbitMQRPC 调用。

可以单独运行 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 重启之后,消息不会丢。要实现完整的持久化,你需要同时设置三个地方:

  1. 队列的 durable 参数设为 true:这样 RabbitMQ 重启后,队列不会丢

  2. 消息的 DeliveryMode 设为 amqp.Persistent:这样消息会被存到磁盘,重启后不会丢

  3. 交换机的 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)

死信队列,就是用来存放那些 死掉 的消息的队列。什么是死信?就是:

  1. 消息被消费者拒绝了,并且没有重新入队

  2. 消息过期了(TTL 到了)

  3. 队列满了,消息放不下了

这些消息不会直接被扔掉,而是会被转发到死信交换机,然后存到死信队列里,我们可以后续排查问题,或者重试处理。

最常见的用法就是用死信队列实现延迟消息:比如我们要做订单超时关闭,下单后 30 分钟没支付,就自动关闭订单。

我们可以这样做:

  1. 声明一个普通队列,设置它的 TTL 是 30 分钟,并且设置死信交换机

  2. 下单的时候,把消息发到这个队列

  3. 30 分钟后,消息过期了,就会被转发到死信队列

  4. 我们的消费者监听死信队列,收到消息后,就去检查订单状态,如果没支付就关闭

代码示例:

// 声明死信交换机
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 是一个非常强大的工具,掌握了它,你就能在分布式系统中轻松实现异步通信、解耦、削峰这些功能。

接下来,你可以自己动手把这些代码跑一遍,加深理解,然后就可以在你的项目中使用它了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NullllllII

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值