第一步:AMQP 协议的两层结构
RabbitMQ 使用的通信协议叫AMQP。这个协议把网络通信拆成了两层:
| 层级 | 对应代码 | 本质 |
|---|---|---|
| Connection | Connection conn = factory.newConnection() | 一条真实的TCP 连接(Socket) |
| Channel | Channel ch = conn.createChannel() | 在这条 TCP 连接上开辟的逻辑会话 |
所有实际的 AMQP 命令(发消息、收消息、声明队列、ACK 确认)都是在Channel上执行的,而不是直接在 Connection 上。
第二步:假设协议没有 Channel 这一层
假设 AMQP 协议设计得非常简单,只有 Connection,所有操作都直接通过 TCP 连接发送。
你的代码可能是这样的:
public class BadProducer { public void sendOrder(Order order) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); // 每次发消息,都新建一个 TCP 连接 Connection conn = factory.newConnection(); // TCP 三次握手 // ... 发消息 ... conn.close(); // TCP 四次挥手 } public void sendPayResult(PayResult result) throws Exception { ConnectionFactory factory = new ConnectionFactory(); // 又新建一个 TCP 连接 Connection conn = factory.newConnection(); // ... conn.close(); } }这会带来什么弊端?
弊端1:TCP 连接建立成本极高一次newConnection()底层要经历:
TCP 三次握手(1 次 RTT)
如果开了 TLS/SSL,还要证书交换(2-3 次 RTT)
AMQP 协议自身的握手(协商版本、认证)
加起来可能要几十到几百毫秒。你发一条消息才几毫秒,建立连接却花了 100ms,性能极差。
弊端2:操作系统资源被快速耗尽每个 TCP 连接都要占用:
一个本地端口
一个文件描述符(fd)
内核里的 socket 发送/接收缓冲区(通常几十 KB)
RabbitMQ 服务端也要为每个连接维护状态(几百 KB 到几 MB)
如果系统有 1000 个线程并发,就要 1000 个 TCP 连接,服务端内存很快就被吃光。
弊端3:频繁创建销毁,GC 压力大
连接对象、缓冲区、协议状态……不断创建和销毁,JVM 垃圾回收频率飙升。
第三步:那能不能只建一个 Connection,所有线程共享?
你可能会想:我创建一个全局的 Connection,所有发消息的操作都用这一个连接,不就行了?
public class SharedConnection { // 全局单例 public static final Connection connection = ...; public void threadA_send() { // 线程 A 直接往 connection 里写数据 } public void threadB_send() { // 线程 B 也往同一个 connection 里写数据 } }问题:多线程并发写同一个 Socket,数据会错乱
AMQP 协议把数据拆成帧(Frame)发送。一个basicPublish命令会被拆成多个帧:
[Frame1: 方法帧][Frame2: 内容头帧][Frame3: 消息体帧]如果线程 A 和线程 B 同时往同一个 TCP Socket 里写:
线程 A: [Frame1-A][Frame2-A]... 线程 B: [Frame1-B][Frame2-B]... 实际发送: [Frame1-A][Frame1-B][Frame2-A][Frame2-B] ← 帧交错,服务端解析乱套所以Connection 不是线程安全的,不能让多个线程裸奔式地并发写同一个 Connection。
第四步:Channel 的设计思路
AMQP 协议的设计者面临一个矛盾:
| 需求 | 限制 |
|---|---|
| 想减少 TCP 连接数,节约资源 | 一个 TCP 连接不能多线程并发乱写 |
| 想让多个线程同时独立工作 | 不能每个线程都新建 TCP 连接 |
解决方案:在一条 TCP 连接上,通过协议层面的 ID 复用,虚拟出多个独立的会话。
这就是Channel。
协议怎么实现的?
AMQP 的每个帧(Frame)头部都有一个字段叫channel number:
┌──────────┬─────────────┬──────────┬──────────┐ │ Frame类型 │ channel ID │ 负载大小 │ 数据体 │ │ (1字节) │ (2字节) │ (4字节) │ │ └──────────┴─────────────┴──────────┴──────────┘线程 A 申请
channel=1,所有操作都带channel=1线程 B 申请
channel=2,所有操作都带channel=2它们共享同一个 TCP Socket 发送数据
RabbitMQ 服务端收到帧后,根据
channel ID把帧路由到对应的内存会话对象
同一个 TCP Connection ├── 线程 A 发送: Frame(channel=1, publish order) ├── 线程 B 发送: Frame(channel=2, consume queue) ├── 线程 A 发送: Frame(channel=1, ack message) └── 线程 B 发送: Frame(channel=2, cancel consumer)Channel 的创建和销毁只是内存里的状态变更(发一个channel.open或channel.close帧),不需要 TCP 握手,所以非常快。
第五步:代码对比
没有 Channel 复用(错误做法)
// 每次操作都经历完整的 TCP 建立和销毁 public void badSend(String msg) throws Exception { Connection conn = factory.newConnection(); // 重!TCP 握手 // 这里即使 AMQP 强制要求 createChannel,但逻辑上如果每个操作都新建 Connection Channel ch = conn.createChannel(); ch.basicPublish("exchange", "key", null, msg.getBytes()); ch.close(); conn.close(); // 重!TCP 挥手 }正确使用 Channel(复用 Connection)
public class GoodProducer { // 一个应用/一个服务节点,通常只维护少量 Connection(甚至一个) private final Connection connection; public GoodProducer() throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); this.connection = factory.newConnection(); // 启动时只建一次 } // 每次发消息,创建一个轻量级的 Channel public void send(String exchange, String routingKey, byte[] msg) throws Exception { Channel ch = connection.createChannel(); // 轻量!只是发一个 open 帧 try { ch.basicPublish(exchange, routingKey, null, msg); } finally { ch.close(); // 只关 Channel,TCP 连接保持不断 } } }第六步:Channel 的线程安全规则
Channel虽然解决了 TCP 连接复用的问题,但它本身不是线程安全的。
// 错误:多个线程共享同一个 Channel public class Wrong { private Channel sharedChannel; // 全局共享 public void threadA() { sharedChannel.basicPublish(...); // 线程 A 发 } public void threadB() { sharedChannel.basicPublish(...); // 线程 B 同时发 → 可能帧交错 } }为什么 Channel 不设计成线程安全的?
因为AMQP 协议要求同一个 Channel 上的命令必须是有序的。比如:
basic.consume("queue") → basic.cancel(consumerTag) → basic.consume("queue2")如果多线程并发执行,顺序就乱了。而且如果给每个 Channel 加锁,吞吐量会大幅下降。
正确做法:
一个线程一个 Channel
或者使用 Spring AMQP 的
RabbitTemplate(内部帮你管理 Channel 缓存)
第七步:Spring 里为什么看不到 Channel?
在实际项目中,你通常这样写:
@Service public class OrderService { @Autowired private RabbitTemplate rabbitTemplate; public void sendOrder(Order order) { // 你看不到 Channel,但它内部确实用了 Channel rabbitTemplate.convertAndSend("order.exchange", "order.key", order); } }Spring 的RabbitTemplate底层做了什么事?
从
CachingConnectionFactory获取一个缓存的 Connection从 Connection 里
createChannel()或,从缓存池里拿一个已有的 Channel在这个 Channel 上执行
basicPublish把 Channel 还回缓存池(不是真的关闭),供下次复用
所以你平时看不到 Channel,但它一直在幕后工作。
总结
| 概念 | 本质 | 重量 | 数量 |
|---|---|---|---|
| Connection | 真实的 TCP Socket 连接 | 重(涉及网络握手、系统资源) | 少(一个服务节点通常几个就够了) |
| Channel | TCP 连接上的逻辑会话,通过channel ID区分 | 轻(纯内存状态,一个帧就创建) | 多(每个线程/每次操作都可以有一个) |
Channel 存在的原因:
为了解决"TCP 连接资源昂贵"和"多线程需要独立会话"之间的矛盾。
没有 Channel,要么频繁创建销毁 TCP 连接压垮系统,要么多个线程竞争同一个连接导致数据错乱。
记住一句话:
Connection 是物理管道(建一次,一直复用)
Channel 是逻辑线路(按需创建, lightweight,用完即还)