消息队列(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. cobbler多机定制安装

    目录 cobbler多机定制安装 1. cobbler服务端部署 2. 客户端安装 3. 定制安装配置 4. 安装 client1开机 client2开机 cobbler多机定制安装 1. cobbl ...

  2. 介绍一种 Python 更方便的爬虫代理池实现方案

    现在搞爬虫,代理是不可或缺的资源 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例的人,却不知道如何去学习更加高深的知识.那 ...

  3. Python 超简单 提取音乐高潮(附批量提取)

    很多时候我们想提取某首歌的副歌部分(俗称 高潮部分),只能手动直接卡点剪切,但是对于大批量的获取就很头疼,如何解决? 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后 ...

  4. eureka和feign的使用

    1 eureka和feign的简介(copy来的) eureka:Eureka是Netflix开发的服务发现组件,本身是一个基于REST的服务.Spring Cloud将它集成在其子项目spring- ...

  5. 前端面试 vue 部分 (5)——VUE组件之间通信的方式有哪些

    VUE组件之间通信的方式有哪些(SSS) 常见使用场景可以分为三类: 父子通信: null 父向子传递数据是通过 props ,子向父是通过 $emit / $on $emit / $bus Vuex ...

  6. 3、Template Method 模板方法 行为型设计模式

    1.了解模板方法 1.1 模式定义 定义一个操作算法中的框架,而将这些步骤延迟加载到子类中. 它的本质就是固定算法框架. 1.2 解决何种问题 让父类控制子类方法的调用顺序 模板方法模式使得子类可以不 ...

  7. Linux查询版本、查询端口

    lsb_release -a 查看当前Linux系统版本 netstat 检查端口 netstat 是一个命令行工具,可以提供有关网络连接的信息.要列出正在侦听的所有 TCP 或 UDP 端口,包括使 ...

  8. 01 树莓派4B—C语言编程——GPIO

    #include <stdio.h>#include <wiringPi.h> int main( void){ int LED1 = 1; int LED4 = 4; wir ...

  9. 旧 WCF 项目迁移到 asp.net core + gRPC 的尝试

    一个月前,公司的运行WCF的windows服务器down掉了,由于 AWS 没有通知,没有能第一时间发现问题. 所以,客户提出将WCF服务由C#改为JAVA,在Linux上面运行:一方面,AWS对Li ...

  10. ubuntu开发机所需工具,做个记录,不断补充

    文件搜索 FSearch 用了下可以, 类似windows下的Everything 或者mac的cmd+空格 地址 安装: sudo add-apt-repository ppa:christian- ...