消息队列(Message Queue,以下简称MQ)常用于异步系统的数据传递。若不用MQ,我们只能[在应用层]使用轮询或接口回调等方式处理,这在效率或耦合度上是难以让人满意的。当然我们也可以在系统间保持一个长连接,基于底层socket机制进行数据的实时收发,如果再将这部分功能独立成一个中间件,供项目中所有系统使用,就是我们今天所指的MQ。


对比&选择

以下以当前较为流行社区活跃度较高的两个MQ——RabbitMQKafka做一比较,顺带提一提redis

简单的小型系统可以使用redis,redis简单易用,本身就提供了队列结构,也支持发布订阅模式。不过说到底redis是一个缓存数据库,主要职责并不是消息队列,缺少消息可达(防丢失)、可靠性(分布式、集群)、异步、事务处理等特性,需要应用层额外处理。

RabbitMQ:erlang开发,单机吞吐高,但是它只支持集群模式,不支持分布式,可靠性依靠的是集群中分属两个不同节点的master queuemirror queue同步数据,在master queue所在节点挂掉之后,系统把mirror queue提升为master queue,负责处理客户端队列操作请求。注意,mirror queue只做镜像,设计目的不是为了承担客户端读写压力,读写都走的master queue,这就有了单点性能瓶颈。RabbitMQ支持消费端pull和push模式。

Kafka:Scala开发,支持分布式,因此如果是相同队列,集群吞吐量肯定是大于RabbitMQ的。Kafka只支持pull模式。pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断轮询,直到新消息到达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞直到消息的数量达到某个特定的量这样就可以批量获取),如此个人认为在消息传输不是很频繁的场景下反而比push更好,即减少了轮询次数,又不需要永远占着一个连接,实时性也基本上能得到保障。RabbitMQ pull模式并不支持此机制。

其实对于吞吐量而言,除非我们预期有百万级并发,否则两者差别不大。另外对于上述RabbitMQ每个队列的单点瓶颈,我们可以将一个队列按一定逻辑拆分为多个队列,在业务端将消息分流,也能提高吞吐量。相比Kafka,RabbitMQ提供了较为完备的消息路由、消息到期删除/延迟/预定、消息容错机制,这些功能可不是短期内靠堆硬件能完成的,对此有要求的话,那么优选RabbitMQ没错了。由此我们也知道了为什么Kafka常用于日志系统,一是日志相对业务来说写操作异常频繁,可能一次请求会产生数十条日志,需要较高的吞吐量,且关联日志一般都是跨系统跨业务的,无法进行细粒度拆分,限制了RabbitMQ提升吞吐量的空间;另外日志记录对一致性实时性等要求不高,不需要什么策略,稍有丢失也无关大雅,无法体现RabbitMQ的优势。

综上所述,业务层建议使用RabbitMQ。


RabbitMQ概念及注意点

RabbitMQ主要概念:

  1. Connection:在RabbitMQ中指的是AMQP 0-9-1 connection,它与底层的TCP链接是一一对应的。
  2. Channel:信道,用于消息的传递。
  3. Queue:队列,消息通过交换机被投递到这里。
  4. Exchange:交换机,用于消息路由,通过routeKey决定将消息投递到哪个队列,有四种模式(fanout、direct、topic、header)。
  5. routeKey:路由键。
  6. DeadLetter:死信机制。

关于它们的介绍网上资料很多,这里就不赘述了,我们把注意点放到具体细节上。以下部分摘自RabbitMQ最佳实践,建议先了解了上述RabbitMQ主要概念再看。

在RabbitMQ中,消息确认分为发送方确认和消费方确认两个确认环节。

  • 发送端:

    ConfirmListener:消息是否到达exchange的回调,需要实现两个方法——handleAckhandleNack。通常来讲,发送端只需要保证消息能够发送到exchange即可,而无需关注消息是否被正确地投递到了某个queue,这个是RabbitMQ和消息的接收方需要考虑的事情。基于此,如果RabbitMQ找不到任何需要投递的queue,那么依然会ack给发送方,此时发送方可以认为消息已经正确投递,而不用关心消息没有queue接收的问题。此时可以为exchange设置alternate-exchange,即表示rabbitmq将把无法投递到任何queue的消息发送到alternate-exchange指定的exchange中,此时该指定的exchange就是一个死信交换机(DLX,所以DLX与普通交换机并无不同,只不过路由的是一些无法处理的消息而已)。

    ReturnListener:事实上,对于exchange存在但是却找不到任何接收queue时,如果发送时设置了mandatory=true,那么在消息被ack前将return给发送端,此时发送端可以创建一个ReturnListener用于接收返回的消息。

    需要注意的是,在发送消息时如果exchange不存在,消息会被直接丢弃,并且不会ack或者nack操作。
  • 消费端:消息默认是直接ack的,即消息到达消费方立即ack,而不管消费方业务处理是否成功。大部分情况我们需要业务处理完毕才认为此消息被正确消费了,为此可以开启手动确认模式,即有消费方自行决定何时应该ack,通过设置autoAck=false开启手动确认模式。

    requeue:消费端nack或reject时设置,告知rq是否将消息重新投递。

    默认情况下,queue中被抛弃的消息将被直接丢掉,但是可以通过设置queue的x-dead-letter-exchange参数,将被抛弃的消息发送到x-dead-letter-exchange中指定的exchange中,这样的exchange成为DLX。

Lazy Queue:一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。惰性队列会将接收到的消息直接存入文件系统中,而不管是持久化的或者是非持久化的。注意如果惰性队列中存储的是非持久化的消息,内存的使用率会一直很稳定,但是重启之后消息一样会丢失。

实测在topic模式下,例如test#是没用的,无法匹配test1,需要配置为test.#,也许这是RabbitMQ所要求的规范吧。

在RabbitMQ中,使用一个还是多个exchange,似乎网上并没有关于这方面的广泛讨论(可见性能上两种方案并无显著差别),so,我们就从不增加复杂度出发,保持一个exchange对应多个queue的简单模式,或按业务划分。

创建链接时可以提供一个ConnectionName,如newConnection(ConnectionName),然而ConnectionName似乎只是给人看的(比如在管理后台),并不要求唯一性。

接下来我们聊下Connection和Channel。


Connection和Channel

为什么要将这两个东西单独拎出来讲呢。因为RabbitMQ并没有为我们提供一个开箱即用的链接复用组件。众所周知,Connection这东西,创建和销毁是一笔不小的开支,然而以官方提供的Java Client SDK为例,ConnectionFactory.newConnection()每次都会new一个新的Connection,SDK并没有内置连接池,这块工作就需要另外处理。

NIO里也有这两个概念(不了解NIO的可参看博主以前写的也谈Reactor模式),而它们都有链接复用的意思,也许RabbitMQ就是参考了NIO呢。

而Channel是干嘛的?数据传输嘛,Connection就做了,有了Connection为什么还要Channel?其实Channel是为了在另外一个层面复用Connection———解决多线程数据并发传输的问题。直接操作Connection进行数据传输,当有多个线程同时操作时,很容易出现数据帧错乱的情况。一个Connection可以create多个Channel。消息的收发、路由等操作都是和某个Channel绑定而非Connection,每个消息都由一个Channel ID标识。然而,窃以为这种细节完全可以对使用者隐藏,暴露出来反而会增加复杂度。关于Channel的使用方式和注意事项,官方文档给出了一些描述,具体编码时需要考量。

As a rule of thumb, Applications should prefer using a Channel per thread instead of sharing the same Channel across multiple threads.

官方建议每个线程使用一个Channel,不同线程最好不要共享Channel,否则并发时容易产生数据帧交错(同多个线程直接共用一个Connection一样),这种情况下exchange会直接关闭下层Connection。

Channels consume resources,所以在一个进程中同时存在成百上千个打开状态的Channel不是一个好注意。但是用完即关也不是一个好主意, A classic anti-pattern to be avoided is opening a channel for each published message. Channels are supposed to be reasonably long-lived and opening a new one is a network round-trip which makes this pattern extremely inefficient. 开辟一个新的channel需要一个网络的往返,这种模式是很低效的。

Consuming in one thread and publishing in another thread on a shared channel can be safe.

一个Connection上的多个Channel的调度是由一个java.util.concurrent.ExecutorService负责的。我们可以使用ConnectionFactory#setSharedExecutor设置自定义调度器。

在消费者端,消息Ack需要由接收消息(received the delivery)的线程完成,否则可能会产生Channel级别的异常,并且Channel会被关闭。

总得来说,Channel也需要复用,但是数量可以比Connection多一两个数量级。我们可以设计一个简单的连接池方案PooledConnectionFactory,它是一个Connection容器,保持若干long-lived Connection提供给外部使用,而每个Connection又有自己的PooledChannelFactory,其中维持着一些long-lived Channel。Spring-boot提供了一个AMQP组件Spring AMQP,已经帮我们实现了类似的方案,并且还隐藏了Channel这个东东,但Spring的原罪——代码碎片化,注解满天飞——又增加了组件本身使用的复杂度,且无法掌控细节。当然它还提供了其它一些可有可无的特性。其实,我们只需要一个简单的连接池而已,so,让我们自己实现吧。


简单连接池实现&使用

直接上代码。先定义一个链接配置类:

@Component
data class ConnectionConfig(
@Value("\${rabbitmq.userName:guest}")
var userName: String,
@Value("\${rabbitmq.password:guest}")
var password: String,
@Value("\${rabbitmq.host:localhost}")
var host: String,
@Value("\${rabbitmq.port:5672}")
var port: Int,
@Value("\${rabbitmq.virtualHost:/}")
var virtualHost: String
)

配置项可在配置文件中配置。

工厂类,内部有个BlockingQueue存放PooledConnection实例,PooledConnection封装了RabbitMQ的Connection,至于为啥要封装一层稍后说。

@Component
class PooledConnectionFactory(@Autowired private val connectionConfig: ConnectionConfig,
@Value("\${rabbitmq.maxConnectionCount:5}")
private val maxConnectionCount: Int) {
private val _logger: Logger by lazy {
LoggerFactory.getLogger(PooledConnectionFactory::class.java)
} private val _connQueue = ArrayBlockingQueue<PooledConnection>(maxConnectionCount) //已创建了几个connection
private val _connCreatedCount = AtomicInteger() private val _factory by lazy {
buildConnectionFactory()
} private fun buildConnectionFactory(): ConnectionFactory {
val factory = ConnectionFactory()
with(connectionConfig) {
factory.username = userName
factory.password = password
factory.virtualHost = virtualHost
factory.host = host
factory.port = port
}
return factory
} @Throws(IOException::class, TimeoutException::class)
fun newConnection(): PooledConnection {
var conn = _connQueue.poll()
if (conn == null) {
if (_connCreatedCount.getAndIncrement() < maxConnectionCount) {
try {
conn = PooledConnection(_factory.newConnection(), _connQueue)
} catch (e: Exception) {
_connCreatedCount.decrementAndGet()
_logger.error("创建RabbitMQ连接出错", e)
throw e
}
} else {
_connCreatedCount.decrementAndGet()
conn = _connQueue.take()
}
}
return conn
}
}

注意newConnection方法使用了AtomicInteger保证线程安全。

再来看PooledConnection,它实现了Closeable接口。而我是用kotlin写的代码,对于Closeable接口,kotlin提供了一个扩展函数use(),use函数会在代码块执行后自动关闭调用者(无论中间是否出现异常),类似于C#的using()操作,等会我们就会看到如何使用。

class PooledConnection(private val connection: Connection, private val container: BlockingQueue<PooledConnection>) : Closeable {
private val _logger: Logger by lazy {
LoggerFactory.getLogger(PooledConnection::class.java)
} override fun close() {
val offered = container.offer(this)
if (!offered) {
val message = "RabbitMQ连接池已满,无法释放当前连接"
_logger.error(message)
throw IOException(message)
}
} fun get() = connection
}

注意close()函数不是真的close,而是将Connection放回连接池。如果用的是RabbitMQ.Connection的话,就直接关闭了。

get()函数将RabbitMQ.Connection暴露出来供生产者和消费者使用。

That's all! 关于连接池的代码就这么简单,Channel池也可照猫画虎,以此类推:)

使用的话,以生产端为例:

    /**
* 发送消息
*
* @param data 需要发送的数据
* @param exchange the name of the exchange sent to
* @param routeKey 路由键,用于exchange投递消息到队列
*/
@Throws(IOException::class)
fun send(data: Any, exchange: String, routeKey: String) = factory.newConnection().use{
val conn = it.get()
val channel = conn.createChannel()
try {
val properties = AMQP.BasicProperties.Builder()
.contentType("application/json")
.deliveryMode(2) //消息持久化,防处理之前丢失。默认1。
.build()
it.basicPublish(exchange, routeKey, properties, JSON.toJSONString(data).toByteArray())
}catch (e: Exception) {
logger.error(e.message)
throw e
} finally {
channel.close()
}
}

so easy! 注意use()的用法。


参考资料

RabbitMQ和Kafka到底怎么选?

Kafka与RabbitMQ区别

RabbitMQ发布订阅实战-实现延时重试队列

RabbitMQ入门指南的更多相关文章

  1. RabbitMQ 入门指南——安装

    RabbitMQ好文 Rabbitmq Java Client Api详解 tohxyblog-博客园-rabbitMQ教程系列 robertohuang-CSDN-rabbitMQ教程系列 Rabb ...

  2. RabbitMQ 入门指南(Java)

    RabbitMQ是一个受欢迎的消息代理,通常用于应用程序之间或者程序的不同组件之间通过消息来进行集成.本文简单介绍了如何使用 RabbitMQ,假定你已经配置好了rabbitmq服务器. Rabbit ...

  3. RabbitMQ 入门指南——初步使用

    MQ的消息持久化 https://www.rabbitmq.com/tutorials/tutorial-two-java.html When RabbitMQ quits or crashes it ...

  4. RabbitMQ入门与使用篇

    介绍 RabbitMQ是一个由erlang开发的基于AMQP(Advanced Message Queue)协议的开源实现.用于在分布式系统中存储转发消息,在易用性.扩展性.高可用性等方面都非常的优秀 ...

  5. .NET 环境中使用RabbitMQ RabbitMQ与Redis队列对比 RabbitMQ入门与使用篇

    .NET 环境中使用RabbitMQ   在企业应用系统领域,会面对不同系统之间的通信.集成与整合,尤其当面临异构系统时,这种分布式的调用与通信变得越发重要.其次,系统中一般会有很多对实时性要求不高的 ...

  6. 《KAFKA官方文档》入门指南(转)

    1.入门指南 1.1简介 Apache的Kafka™是一个分布式流平台(a distributed streaming platform).这到底意味着什么? 我们认为,一个流处理平台应该具有三个关键 ...

  7. 【RabbitMQ 实战指南】一 RabbitMQ 开发

    1.RabbitMQ 安装 RabbitMQ 的安装可以参考官方文档:https://www.rabbitmq.com/download.html 2.管理页面 rabbitmq-management ...

  8. RabbitMQ入门看这一篇就够了

    一文搞懂 RabbitMQ 的重要概念以及安装 一 RabbitMQ 介绍 这部分参考了 <RabbitMQ实战指南>这本书的第 1 章和第 2 章. 1.1 RabbitMQ 简介 Ra ...

  9. Web API 入门指南 - 闲话安全

    Web API入门指南有些朋友回复问了些安全方面的问题,安全方面可以写的东西实在太多了,这里尽量围绕着Web API的安全性来展开,介绍一些安全的基本概念,常见安全隐患.相关的防御技巧以及Web AP ...

随机推荐

  1. CF EC 87 div2 1354 C2 Not So Simple Polygon Embedding 计算几何 结论

    LINK:Not So Simple Polygon Embedding 搞了好久终于搞会了. 错误原因 没找到合适算边长的方法 要么就是边长算的时候算错了. 几何学的太差了 最后虽然把十边形的和六边 ...

  2. 一本通 1783 矩阵填数 状压dp 容斥 计数

    LINK:矩阵填数 刚看到题目的时候感觉是无从下手的. 可以看到有n<=2的点 两个矩形. 如果只有一个矩形 矩形外的方案数容易计算考虑 矩形内的 必须要存在x这个最大值 且所有值<=x. ...

  3. CF R 635 div2 1337D Xenia and Colorful Gems 贪心 二分 双指针

    LINK:Xenia and Colorful Gems 考试的时候没想到一个很好的做法. 赛后也有一个想法. 可以考虑答案的样子 x,y,z 可以发现 一共有 x<=y<=z,z< ...

  4. luogu P5043 【模板】树同构 hash 最小表示法

    LINK:模板 树同构 题目说的很迷 给了一棵有根树 但是重新标号 言外之意还是一棵无根树 然后要求判断是否重构. 由于时无根的 所以一个比较显然的想法暴力枚举根. 然后做树hash或者树的最小表示法 ...

  5. JavaSwing+Mysql实现简单的登录界面+用户是否存在验证

    原生Java+mysql登录验证 client login.java 功能:实现登录页面,与服务端传来的数据验证 package LoginRegister; import java.awt.Cont ...

  6. CentOS部署RabbitMQ

    CentOS版本:CentOS-7-x86_64-DVD-1804 RabbitMQ版本:3.7.24 1. 下载安装包 因为RabbitMQ是erlang语言开发的,所以需要提前安装erlang环境 ...

  7. 使用pytorch快速搭建神经网络实现二分类任务(包含示例)

    使用pytorch快速搭建神经网络实现二分类任务(包含示例) Introduce 上一篇学习笔记介绍了不使用pytorch包装好的神经网络框架实现logistic回归模型,并且根据autograd实现 ...

  8. Python数据类型-dic,set常见操作

    字典常见方法   语法:字典名[新key]=value 功能:给字典增加键值 语法:字典名[字典里存在的key]=新的value 功能:修改字典里的值   功能:删除字典的元素,通过key来进行删除, ...

  9. JVM与Java体系结构

    参考笔记:https://blog.csdn.net/weixin_45759791/article/details/107322503 前言 作为Java工程师的你曾被伤害过吗?你是否也遇到过这些问 ...

  10. java_内部类、匿名内部类的使用

    内部类 将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类. 内部类的分类 成员内部类,类定义在了成员位置 (类中方法外称为成员位置) 局部内部类,类定义在方法内 成员内部类 ...