总结:RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,ActiveMQ比较成熟,但是性能对比其他比较差
扩展:
RocketMQ的分布式事务等功能是需要收费。
Erlang语言常用在socket编程
AMQP协议
在2003年被提出,最早用于解决金融领域不同平台之间的消息传递交互问题。准确的说是一种 binary wire-levelprotocol(链接协议)。AMQP不是从API层进行限定,而是直接定义网络交换 的数据格式,这使得实现AMQP的provider是具有天然的跨平台性。下图是AMQP的模型
生产者将消息发送给交换机,而消费者只绑定相应的消息队列即可,而交换机与消息队列之间也有多种从属关系。基于这种模型,可以实现多种消息发送。
RabbitMQ官网
在官网首页Get Started中 点击download + installation,在这里rabbitMQ提供了Linux、Windows、MacOS的各个系统安装包,并且由于RabbitMQ是用Erlang语言开发的,也要下载Erlang语言的依赖包,这个也在官网中有提供。推荐在centos中安装。
下面以centos为例安装
rmp -ivh erlang-22.0.7-1.e17.x86_64.rpm
yum install -y rabbitmq-server-3.7.18-1.e17.noarch.rpm
cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
find / -name rabbitmq.config.example
该命令表示从根目录下开始查找一个名字叫xxx的文件vim /etc/rabbitmq/rabbitmq.config
,如果系统不支持vim命令,可以使用命令yum install -y vim
安装vimsystemctl status rabbitmq-server
来启动systemctl start rabbitmq-server
启动rabbitmq服务systemctl stop rabbitmq-server
停止rabbitmq服务systemctl restart rabbitmq-server
重启rabbitmq服务systemctl status rabbitmq-server
查看rabbitmq服务的状态systemctl status firewalld.service
查看防火墙状态rabbitmq服务的启动停止命令上面已经介绍了,那么在使用命令行的方式来操作rabbitmq也是与之类似,使用rabbitmqctl
命令来操作,具体可以使用help命令来查看对应的操作,这里简单介绍,语法如下
使用该命令可以对rabbitmq 的节点、集群、用户、消息队列等等都可以操作配置,这里不多叙述
当我们安装rabbitmq的时候是没有web管理界面的,这个时候可以使用命令rabbitmq-plugin enable rabbitmq_management
开启web管理界面
也可以使用rabbitmq-plugins
命令来查看
还可以使用rabbitmq-plugins list
来查看rabbitmq的所有插件,可以使用enable disable 来启用禁用各个插件
web管理界面默认账号 guest 密码相同,拥有超级管理员权限,可以访问所有主机。
这里的新建用户ems虽然是管理员权限但是是没有访问虚拟主机的权限的,因此这里要给添加,首先点击页面右边的virtual hosts添加虚拟主机,然后点击用户名ems进入设置页面,在set permission中设置虚拟主机的访问权限。
这里介绍了整个rabbitmq 的所有情况总览如下图
5672是消息通信服务端口
15672是web管理界面的端口
25672是集群的端口
设置虚拟主机的最大连接数和最大队列数
Rabbitmq基于生产者消费者模型实现系统之间的解耦
生产者通过通道 连接虚拟主机,需要账号密码,将消息放到交换机中,也可以直接将消息放入队列queue中
一个业务或一个系统绑定一个虚拟主机
消费者与生产者是完全解耦的,不需要关注生产者是否生产或者在线
消费者也需要连接到rabbit的虚拟主机,然后才能从主机中的队列中获取消息
在rabbitmq的官网中可以找到给我们提供的消息发送策略,rabbitmq文档
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.10.0version>
dependency>
p表示生产者provider
c表示消费者 customer
红色的表示队列queue
生产者
@Test
public void provider() throws IOException, TimeoutException {
//获取rabbitmq连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置主机
connectionFactory.setHost("192.168.0.113");
//设置端口
connectionFactory.setPort(5672);
//设置连接的虚拟主机,一般情况我们是一个子项目或一个业务模块对应一个虚拟主机,方便区分,如果没有
//则可以通过web页面添加虚拟主机
connectionFactory.setVirtualHost("ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接的通道
Channel channel = connection.createChannel();
//点对点的消息传递模型中没有交换机的概念,只有生产者通过通道channel将消息发送到队列中,
//然后消费者消费,因此接下来要简历通道channel与队列queue的关系
//绑定队列,如果这个时候没有队列,则也是一样,在web页面中新建队列,如果不新建,则这里会默认使用当前的名字来创建一个queue
//参数1:表示队列的名称
//参数2:表示是否要持久化消息,如果设置为false,则下次重启mq后消息丢失
//参数3:表示是否独占队列,为true表示该队列仅当前链接可用,其他链接不能用,一般情况为false
//参数4:表示是否在消费完成后自动删除该队列,true表示自动删除,
//参数5:附加参数
channel.queueDeclare("hello", false, false ,false, null);
//发布消息
//参数1:表示交换机
//参数2:表示队列
//参数3:表示传递消息的额外设置
//参数4:表示要发送的消息体
channel.basicPublish("","hello",null,"hello rabbitmq".getBytes());
//发送完成关闭资源
channel.close();
connection.close();
}
通道与队列绑定参数详解:
在上面的代码中,我们通过通道向队列中发送消息之前要将通道与队列绑定,即channel.queueDeclare("hello", false, false ,false, null);
这段代码,那么针对这些参数,在这里做个详细的描述:
参数1(queue):队列的名称,如果该队列不存在,则自动创建该队列。如果通道在发送消息的时候没有与队列绑定,会怎么样呢?
参数2(durable):是否持久化,当设置为false的时候,该队列将不会被持久化到磁盘中,那么一旦服务重启,则该队列将不存在,队列中没有消费的消息也将丢失。当设置为true的时候,队列会保存在磁盘中,在下图的feature中会出现一个D标识,表示队列持久化Durable的意思,如果这个时候我们又重启了rabbitmq的服务,这个时候就会发现,队列是存在的,但是队列中没有消费的消息不存在了,这是因为这个参数仅设置了队列的持久化,而消息是没有持久化的。那么如何将消息也持久化呢?请看下面的发布消息参数详解。
参数3:是否独占队列,一般为false
参数4:消费完成后是否自动删除队列,当设置为true的时候,web页面的queues选项中该队列的feature选项会多出一个AD的标识,表示autoDelete。所谓的消费完成表示消费者与队列的连接断开,而不是队列的消息内容为0被消费完。
发布消息参数详解:
channel.basicPublish("","hello",null,"hello rabbitmq".getBytes());
参数1:交换机名称
参数2:队列名称
参数3:附加参数,一个类型为BasicProperties的常量,使用MessageProperties的属性设置为PERSISTENT_TEXT_PLAIN即表示消息持久化。其他属性的意思:
参数4:发送的消息体
消费消息
public static void main(String[] args) throws IOException, TimeoutException {
//与发送消息相同,消费者也是首先要获取到对应的队列,然后才能消费
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.0.113");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("ems");
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", false, false ,false, null);
//消费消息
//参数1:队列的名称
//参数2:开始消息的自动确认机制,详情请看广播模型中的消息自动确认
//参数3:表示消费时的回调,需要将通道传入这个消费者中,这个消费者才知道是从哪个通道去消费,然后这个参数是一个
//consumer类型的,这个consumer是一个接口,defaultConsumer是他的实现类,表示消费结果的回调函数,因此我们要
//重写回调函数中的一个方法
channel.basicConsume("hello",true,new DefaultConsumer(channel){
//回调参数1:
//回调参数2:
//回调参数3:
//回调参数4:body就是我们要消费的消息。
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println(new String(body));
}
});
// channel.close();
// connection.close();
}
可以看到这里的消费者是写在main方法中的,因为消费者需要一直监听队列是否有新的消息并及时处理,并且因为消费者中有消费消息 的回调函数,因此一旦写在Junit单元测试中或者打开channel.close和connection.close都会导致回调方法在没有执行的时候主线程就已经结束了。因此在消费者端不建议关闭通道和连接,除非仅消费一次。
优化
对于上面的代码我们可以发现获取连接关闭连接等等操作都是重复性的,因此要抽取出来做一个工具类
public class RabbitMQUtils {
//由于connectionFactory是一个重量级的资源,我们不能在每次获取连接的时候都新建一个,
//因此我们需要在类加载的时候创建一次就够了。基于这种考虑将工厂的初始化放在了静态代码块中
//随着类的加载而加载
private static ConnectionFactory connectionFactory;
static {
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.0.113");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("ems");
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
}
public static Connection getConnection(){
try{
return connectionFactory.newConnection();
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
最后:发送者和消费者的队列属性必须严格一致,例如发送者设置为持久化队列、持久化消息、不自动删除、不独占队列,则消费者也必须一致,否则会导致消息丢失或报异常
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消费消息的速度,场次以往就会产生消息堆积,无法及时处理。此时就可以使用这种模型,让多个消费者绑定到一个队列 ,共同消费队列中的消息。队列中的消息一旦消费掉就会消失,因此不存在重复执行任务的情况。
生产者:给队列中放入10条消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("","hello2",
MessageProperties.PERSISTENT_TEXT_PLAIN,(i+"hello rabbitmq").getBytes());
}
2个消费者同时监听这个队列,两个消费者的代码相同
channel.basicConsume("hello",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1======"+new String(body));
}
});
查看结果:
通过这个例子可以发现,在没有做其他配置的情况下,两个消费者对消息的消费是相等的,一个 是13579 另一个是2468 10
同时官方文档也说明:默认情况下(work queues)RabbitMQ将按照顺序将每个消息发送给下一个使用者,每个消费者将会收到相同数量的消息。这中分发消息的方式称为循环。
在实际生产中可能会存在消费者1处理的慢而消费者2处理的快,这个时候这种循环的方式就不太合适,因为例如每个都分发10个消息,2已经处理完了,1还没有处理完,总体来讲性能依然是被1拖慢,因此我们希望2可以多处理一些,1可以少处理一些。如何实现能者多劳这种模式呢?如下:
消息确认机制
通过上面的代码channel.basicConsume("hello",true,new DefaultConsumer(channel){
中我们知道,第一个参数是队列的名称,第二个参数表示是否启用消息自动确认,true表示启用。那么rabbitmq是如何自动确认的呢?
当我们使用循环的方式分发消息的时候,队列会将消息均分后全部统一的发送给对应的消费者,发送成功后立即标记删除这些消息。这个时候消息已经到了消费者这边了,一旦这个时候消费者在处理消息的途中宕机或进程意外终止,则没有处理的消息就会丢失。因此我们工作 中不建议开启自动确认机制,我们希望当某个消费者终止后,可以将未处理完的消息转交给其他正常的消费者。如何实现?如下
Channel channel = connection.createChannel();
//每次只能消费一个消息
channel.basicQos(1);
//var1 队列名称 var2是否持久化 var3是否独占 var4完成后是否自动删除队列
channel.queueDeclare("hello", false, false ,false, null);
//消费消息
//参数1:队列的名称
//参数2:消息的自动确认机制
channel.basicConsume("hello",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
//手动确认消息,调用下面这个方法
//参数1:消息的编号,通过这个编号队列才知道是哪个消息被消费了,然后才能确认删除该消息
//参数2:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
上面这段代码的意思是:当我们关闭了自动确认开启手动确认后,并且设置了消费通道每次消费1条消息的时候,队列每次会给每个消费者发送一个消息,并且会等待消费者确认后才会标记删除该消息,如果发送的消息没被确认,则队列中这条消息就不会被删除。这样就可以保证当任意一个消费者宕机或下线后,依然可以让其他未被消费的消息转到其他正常的消费者中。一旦出现了未确认的消息,在web页面中会如下显示,当unacked不为0的时候表示,这些消息已经发送,但没有收到ack确认。
如若处理过程中出现异常,而没有回复ack 应答。通过后台就会看到有 unacked 的数据。
如果积压的多会导致程序无法继续消费数据(数量和消费者的线程数有关)。
解决办法 针对异常 做处理,捕捉到后 也回复ack应答。
程序断开于rabbitmq的链接后 unacked的消息状态会重新变为ready 等待消费。
代码更新后,server应用连接rabbitmq 就会重新消费掉消息。
发布订阅就是先将消息发送到交换机,由交换机决定将消息推送到哪个队列,然后消费者再从队列中消费消息
发送流程:
生产者
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
//channel.exchangeBind("","","");
//参数1:交换机名称,如果交换机不存在,则会自动创建一个交换机
//参数2:交换机类型,fanout表示广播。最新版本的RabbitMQ有四种交换机类型,
//分别是Direct exchange(处理路由键):与fanout的区别就是这种模式会判断routekey,具体请看
// Fanout exchange(广播):一般情况下交换机分发消息会先找到绑定的队列,然后判断routekey(路由键),来决定是否将消息分发到某个队列中,但如果交换机类型是fanout,就不会判断routekey,而是直接将消息发送到绑定的队列中
// Topic exchange、
// Headers exchange。前三个比较常用
channel.exchangeDeclare("logs","fanout");
//发送消息
//参数1:交换机的名称
//参数2:路由key,在广播模式中,路由key是没有任何意义的.具体请参考《交换机类型》
//参数3:消息持久化特性
//参数4:发送的消息内容
channel.basicPublish("log","", null,"一个比斗得有多大的伤害".getBytes());
RabbitMQUtils.closeConAndChannel(channel,connection);
}
}
消费者
对消费者来说,每个消费者都有一个零时的队列,然后将这个零时的队列与交换机绑定,因此不需要指定一个固定的队列,只需要新建一个零时队列,用完删除即可
public class Consume {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
/* exchangeBind 与 exchangeDeclare的区别?????
* @param destination 需要订阅的交换器
* @param source 被绑定的交换器
* @param routingKey 路邮键
* @param arguments 定义绑定的参数
* channel.exchangeBind("","logs","fanout",null);
*/
//绑定交换机,参数1交换机名称 参数2交换机类型
channel.exchangeDeclare("logs","fanout");
//使用该方法返回一个零时的队列,然后将该队列与交换机绑定即可
String queueName = channel.queueDeclare().getQueue();
//将队列通过通道与交换机绑定
// 参数1: queue
// 参数2: exchange
// 参数3: routingKey,路由键,在fanout模式中没用
channel.queueBind(queueName,"logs","");
// 消费消息,开启自动确认
channel.basicConsume(queueName, true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1"+new String(body));
}
});
}
}
fanout模式中消费者应该是有多个,但是代码相同,测试结果:生产者发送消息后,其他消费者均接收到同一个消息。
参考汤青松博客