官网: Messaging that just works — RabbitMQ
什么是MQ
MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。一般用来解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。
几种常见的消息中间件:
RabbitMQ
是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了一个经纪人(Broker)构架,这意味着消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)或者数据持久化都有很好的支持。
Redis
是一个Key-Value的NoSQL数据库,开发维护很活跃,虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。
ZeroMQ
号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演了这个服务角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。***但是ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。***其中,Twitter的Storm中使用ZeroMQ作为数据流的传输。
ActiveMQ
是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。RabbitMQ、ZeroMQ、ActiveMQ均支持常用的多种语言客户端 C++、Java、.Net,、Python、 Php、 Ruby等。
Jafka/Kafka
Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现复杂均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制来统一了在线和离线的消息处理,这一点也是本课题所研究系统所看重的。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
为什么选择RabbitMQ
1、除了Qpid,RabbitMQ是唯一一个实现了AMQP标准的消息服务器;
2、可靠性,RabbitMQ的持久化支持,保证了消息的稳定性;
3、高并发,RabbitMQ使用了Erlang开发语言,Erlang是为电话交换机开发的语言,天生自带高并发光环,和高可用特性;
4、集群部署简单,正是应为Erlang使得RabbitMQ集群部署变的超级简单;
5、社区活跃度高,根据网上资料来看,RabbitMQ也是首选;
RabbitMq入门
RabbitMQ网站端口号:15672
工作机制
生产者、消费者和代理
在了解消息通讯之前首先要了解3个概念:生产者、消费者和代理。
生产者:消息的创建者,负责创建和推送数据到消息服务器;
消费者:消息的接收方,用于处理数据和确认消息;
代理:就是RabbitMQ本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。
在mabbitmq中有7中模型,分别介绍了rabbitmq的7种工作方式,在这里只介绍前5中。
首先应该先导包。
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.12.0</version>
</dependency>
1.“Hello World!”
在这个模型中只有三个对象,生产者,队列,消费者。这是一个最简单的模型。
provider.java
public class provider {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/joviehost");
factory.setUsername("jovie");
factory.setPassword("123456");
factory.setHost("localhost");
// 获取连接
Connection connection = factory.newConnection();
// 获取通道
Channel channel = connection.createChannel();
// 定义队列
channel.queueDeclare("hello",false,false,false,null);
// 发布消息到队列中
channel.basicPublish("","hello",null, "this is a message".getBytes());
// 关闭通道
channel.close();
// 关闭连接
connection.close();
}
}
consumer.java
public class consumer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/joviehost");
factory.setUsername("jovie");
factory.setPassword("123456");
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello",false,false,false,null);
// 消费消息
channel.basicConsume("hello",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println(s);
}
});
}
}
这样就可以实现第一个模型了。
接下来看一下代码:
channel.queueDeclare("hello",false,false,false,null);
//第一个参数queue 为队列名称
//第二个参数durable 表示队列是否支持持久化
//第三个参数exclusive 表示只对首次声明的连接可见,并且在连接断开后自动删除
//第四个参数autodelete 是否自动删除,为true时,当所有连接都断开时将自动删除队列
//第五个参数arguments 附加参数
注意:
-
在rabbitmq中,没有声明durable的交换机和队列在rabbitmq重启时都将被删除。
-
在provider和consumer中声明队列时参数需要一致,否则consumer将无法获取消息。
channel.basicPublish("","hello",null, "this is a message".getBytes());
//第一个参数exchange 交换机名称
//第二个参数routingkey 绑定queue与exchange,在这里没有定义exchange直接填写queue的名称
//第三个参数props 附加消息
//第四个参数message 要发送的消息
注意:
需要时消息在rabbitmq宕机后保存消息(消息持久化)必须增加一些设置,而在定义队列时的持久化设置是使队列不会被删除,而队列中的消息依旧会被删除。使消息持久化应当设置props
channel.basicPublish("","hello", MessageProperties.PERSISTENT_TEXT_PLAIN, "this is a message".getBytes());
channel.basicConsume("hello",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println(s);
}
});
//第一个参数 消费队列的名称
//第二个参数 autoack:1、当 autoAck设置为true时,也就是自动确认模式,一旦消息队列将消息发送给消息消费者后,就会从内存中将这个消息删除。
// 2、当autoAck设置为false时,也就是手动模式,如果此时的有一个消费者宕机,消息队列就会将这条消息继续发送给其他的消费者,这样数据在消息消费者集群的环境下,也就算是不丢失了。
//第三个参数 自定义consumer
2. Work queues
在这个模型中有两个消费者。
当我们将消费者增加到两个时,我们发现,平均分配,不管消费者有没有用完消息。
这样子不太好,当rabbitmq已经平均分配好了消息给消费者后,若消费者宕机了,那么这些消息将丢失。
所以我们取消自动确认机制,改为手动确认。
将第一个消费者延时设置为0.5s,第二个设置为0.3s
这样的模式就是能者多劳。
channel.basicAck(envelope.getDeliveryTag(), false);
//第一个参数指定 deliverytag,第二个参数说明如何处理这个失败消息。requeue 值为 true 表示该消息重新放回队列头,值为 false 表示放弃这条消息。
在进入第三个案例之前,先提取一下冗余代码,使之成为工具类的一部分。
public class QueueUtil {
private static ConnectionFactory factory = new ConnectionFactory();
public static Connection GetConnection(String host,String VirtualHost,String username,String password) throws IOException, TimeoutException {
factory.setHost(host);
factory.setPassword(password);
factory.setUsername(username);
factory.setVirtualHost(VirtualHost);
Connection connection = factory.newConnection();
return connection;
}
public static void CloseConnection(Connection connection, Channel channel) throws IOException, TimeoutException {
if (channel != null){
channel.close();
}
if (connection != null){
connection.close();
}
}
}
3.交换机类型:
Direct 类型
Direct 类型的交换器由路由规则很简单,它会把消息路由到那些 BindingKey 和 RoutingKey 完全匹配的队列中。
Direct Exchange 是 RabbitMQ 默认的交换器模式,也是最简单的模式。它根据 RoutingKey 完全匹配去寻找队列。
Topic 类型
上面讲到 Direct 类型的交换器由规则是完全匹配 BindingKey 和 RoutingKey,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。Topic 类型的交换器在匹配规则上进行了扩展,它与 Direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定:
(1)RoutingKey 为一个点号 “.” 分隔的字符串(被点号 “.” 分隔开的每一段独立的字符串称为一个单词),如:com.rabbitmq.client、java.util.concurrent、com.hidden.client;
(2)BindingKey 和 RoutingKey 一样也是点号 “.” 分隔的字符串;
(3)BindingKey 中可以存在两种特殊字符串星号 “” 和井号 “#”,用于做模糊匹配,其中星号 “” 用于匹配一个单词,井号 “#”用于匹配多个规则单词(0个或者多个单词);
Fanout 类型
消息广播的模式,即将消息广播到所有绑定到它的队列中,而不考虑 RoutingKey 的值(不管路键或是路由模式)。如果设置了 RoutingKey ,则 RoutingKey 依然被忽略。
Headers 类型
Headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。Headers 类型的交换器性能会很差,而且不实用,基本上不会看到它的存在。
4. Publish/Subscribe
在这个模式中,多了一个交换机,消息提供者直接将消息发送给交换机,在按照分发类型将消息发放给队列。
consumer.java
public class consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = QueueUtil.GetConnection("localhost","/joviehost","jovie","123456");
Channel channel = connection.createChannel();
channel.exchangeDeclare("subscribe","fanout",true,false,null);
channel.queueDeclare("test2",false,false,false,null);
channel.queueBind("test2","subscribe","",null);
channel.basicQos(1);
channel.basicConsume("test2",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println(s);
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
provider.java
public class provider {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = QueueUtil.GetConnection("localhost","/joviehost","jovie","123456");
Channel channel = connection.createChannel();
channel.exchangeDeclare("subscribe","fanout",true,false,null);
for (int i = 0; i < 10; i++) {
channel.basicPublish("subscribe","", null, ("this is a message " + i).getBytes());
}
QueueUtil.CloseConnection(connection,channel);
}
}
接下来分析代码:
channel.exchangeDeclare("subscribe","fanout",true,false,null);
//第一个参数 定义交换机名称
//第二个参数 设置交换机类型
//第三个参数 durable是否持久化,和队列一样,设置为false时重启将被删除
//第四个参数 autodelete 是否自动删除,为true时将在没有队列与它绑定时自动删除
//第五个参数 arguments 额外参数
channel.basicPublish("subscribe","", null, ("this is a message " + i).getBytes());
//第一个填交换机名称,第二个参数routingkey,由于时fanout模式,所以这里不用设置。
channel.exchangeDeclare("subscribe","fanout",true,false,null);
//定义交换机
channel.queueDeclare("test2",false,false,false,null);
//定义队列
channel.queueBind("test2","subscribe","",null);
//将队列与交换机绑定,第三个参数为routingkey,fanout模式下不用设置
在 fanout 模式下交换机将会将消息分发给所有与它绑定的队列,绑定的队列都会得到消息
5. Routing
在这个模型中,交换机类型为direct,它根据routingkey与bindingkey匹配将消息分发到队列中。
provider.java
public class provider {
private static final String ExchangeName = "directTest";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = QueueUtil.GetConnection("localhost","/joviehost","jovie","123456");
Channel channel = connection.createChannel();
channel.exchangeDeclare(ExchangeName,"direct",true,false,null);
channel.basicPublish(ExchangeName, "info", null, ("this is an info message").getBytes());
channel.basicPublish(ExchangeName, "error", null, ("this is an error message").getBytes());
channel.basicPublish(ExchangeName, "warning", null, ("this is a warning message").getBytes());
QueueUtil.CloseConnection(connection,channel);
}
}
consumer1.java
public class consumer1 {
private static final String QueueName = "test2";
private static final String ExchangeName = "directTest";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = QueueUtil.GetConnection("localhost","/joviehost","jovie","123456");
Channel channel = connection.createChannel();
channel.exchangeDeclare(ExchangeName,"direct",true,false,null);
channel.queueDeclare(QueueName,false,false,false,null);
channel.queueBind(QueueName,ExchangeName,"info",null);
channel.basicQos(1);
channel.basicConsume(QueueName,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("this is consumer1, and the message is :" + s);
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
consumer2.java
public class consumer2 {
private static final String QueueName = "test1";
private static final String ExchangeName = "directTest";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = QueueUtil.GetConnection("localhost","/joviehost","jovie","123456");
Channel channel = connection.createChannel();
channel.exchangeDeclare(ExchangeName,"direct",true,false,null);
channel.queueDeclare(QueueName,false,false,false,null);
channel.queueBind(QueueName,ExchangeName,"error",null);
channel.queueBind(QueueName,ExchangeName,"warning",null);
channel.queueBind(QueueName,ExchangeName,"info",null);
channel.basicQos(1);
channel.basicConsume(QueueName,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("this is consumer2, and the message is :" + s);
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
在direct模式中,将routingkey与bindingkey匹配的消息发送给对应队列
6. Topics
在这个模型中使用topic类型的交换机,即模糊匹配
一个消息提供者,三个消费者,设置好后运行。
一个 #
可以匹配所有的key,而一个 *
匹配了一个单词。