提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!
之前我的 这篇文章 中介绍过 JUC 中的阻塞队列 BlockingQueue , 介绍过"生产者消费者模型", 在实际开发中, 尤其是分布式系统⾥, 跨主机之间使⽤⽣产者消费者模型, 也是⾮常普遍的需求
因此, 我们通常会把阻塞队列, 封装成⼀个独⽴的服务器程序, 并且赋予其更丰富的功能, 这样的程序我们就称为 消息队列 (Message Queue, MQ)
目前市场上比较成熟的消息队列有: kafka, RabbitMQ, RocketMQ…, 本项目中部分参考 RabbitMQ 和 “AMQP 协议” 进行设计
项目描述 : 可跨主机的生产者消费者服务器程序,用于服务器之间的解耦,流量削峰;使用RPC模式,由客户端发送网络请求,远程调用服务器以操作交换机、队列、发布消息、订阅消息等;由服务器实现相关逻辑、持久化存储、异步转发消息等
所以这个项目中有一些比较核心的概念和模块 :
消费者消费数据由两种模式 :
1, 消费者从队列里取, 这种模式称为"拉"
2, 队列里有消息时, Broker (中间人服务器) 给消费者推送消息, 这种模式称为"推"
我们的项目中实现 “推” 这种模式
Producer, Consumer, Broker 都是服务器程序, 需要编写代码支持这三者之间的业务逻辑
但 Producer, Consumer 又是广义上的消费者, Producer, Consumer 是发起请求的一方, 所以是客户端, Broker 是返回响应的一方, 所以是服务器
(我们的项目中暂时只设定一个VirtualHost)
用数据库做对比:
VirtualHost 相当于 database, Exchange 和 Queue 都是 table, Exchange 和 Queue 可理解成 “多对多” 关系. 所以 Binding 就是一个中间表, 可以把这两张表联系起来
AMQP 协议就是按照上述格式组织
Producer, Consumer 是发起请求的一方, 所以是客户端, Broker 是返回响应的一方, 所以是服务器
因此 BrokerServer 需要给 Producer, Consumer 提供一些核心 API (对外暴露的), 这些 API 用来实现整个消息队列支持的基本功能
我们的项目给消费者在订阅消息时, 提供 “自动应答” 和 “确认应答” 两种方式
还有 “拒绝应答” 这种方式, 本项目暂未实现
没有 “消费消息” 这个 API , 上文已经说明, 我们的项目中不支持消费者主动从队列中取消息, 而是由服务器推送给消费者**(这个过程有些复杂, 后续会详细介绍)**
咱们的项目不实现第四种, 实现方式复杂且比较少见
生产者在发布消息时, 有三个参数: 1, 消息 2, 交换机 3, routingKey (下面介绍), 不同的交换机有不同的转发规则
主题交换机: 不需要和队列建立绑定, 生产者发布消息时, routingKey 作为队列名(唯一标识)
, 如果队列存在, 交换机直接把消息投入到该队列中
扇出交换机: 需要和队列建立绑定, 生产者发布消息时什么都不用做 routingKey 为 null
, 交换机会把消息给已绑定的队列都投送一份
主题交换机: 需要和队列建立绑定, 并且绑定的时候
指定一个交换机和队列之间
的 “暗号” bindingKey
, 生产者发布消息的时候
指定一个 “暗号” routingKey
, 交换机会把消息投送给已绑定的队列中, 暗号能够对上(规则匹配)
的那个队列
Binding 表中就记录了 交换机和队列的绑定关系, 以及 bindingKey
目前不需要全部看懂 Binding 表, 大概熟悉这三种绑定规则即可
上述提到的交换机, 队列, 绑定, 消息 需要在硬盘和内存上各存储一份, 内存为主, 硬盘为辅
因为内存的访问速度快, 而这对消息队列这个项目, 效率更重要, 而硬盘存储是为了服务器重启之后数据不丢失
硬盘存储使用 数据库 + 文件的方式, 数据库中存储 交换机, 队列, 绑定, 文件中存储消息
因为数据库提供的 增, 删, 查 比较方便, 交换机, 队列, 绑定正需要这些操作, 而消息不需要大量, 复杂的增删查, 基本只涉及到存储, 所以使用数据库略显"多余", 而且一旦消息很多之后, 数据库的性能也有限
生产者消费者作为客户端程序, 和 BrokerServer 通过网络进行通信和交互
后续会自定义应用层协议, 传输层使用 TCP 的 Socket API
上述介绍过 BrokerServer 提供的一些核心 API , 在生产者消费者程序上也需要提供的相应的 API
以创建队列这个操作为例: 客户端调用 queueDeclare(), 客户端的这个方法只是实现了向服务器发送请求, 让服务器调用服务器的 queueDeclare() , 服务器的这个方法实现了真正创建交换机, 并持久化存储, 然后返回给客户端一个响应, 告知客户端执行成功了没有
由于客户端也有 queueDeclare() , 客户端看起来是在调用自己的 queueDeclare() 从而创建出来了队列, 实际上是通过网络请求, 调用了服务器的 queueDeclare() , 而服务器是怎么做的, 客户端并不知道细节
这种方式称作: 远程过程调用(RPC)
上面说到要在生产者消费者客户端提供 BrokerServer 的核心 API :
由于一次 TCP 连接与断开 需要经过三次握手四次挥手, 一个客户端有可能短时间内持续使用 RPC 模式远程调用 BrokerServer 的 API, 这就需要多次 TCP 的连接和断开
所以引入 Channel (信道) 的概念, 建立 TCP 连接之后, 可以使用一个 Channel表示一次逻辑上的连接(一次请求和响应), 一个 Connection 中包含多个 Channel , 各个 Channel 中的数据互不相干, 实现一次 TCP 连接的复用
所以生产者客消费者(客户端)还需要提供四个 API
这里各位大概率是一眼看不懂的, 只需要结合注释大致了解各个包和类是用来做什么的即可