Author xiuhongChen
Date 2018/9/28
Desc RabbitMQ基本概念、队列模型、安装教程、Java应用、集群搭建等
核心特性:
(1) 异步消息,支持多种消息协议、消息队列、传送确认、灵活地路由到消息队列、有多种的交换类型。
(2) 分布式部署,可以实现非常方便地部署负载均衡的集群,实现高可用以及高的吞吐量。
(3) 插件丰富,提供各种各样的工具和插件,支持持续集成,运营指标和与其他企业系统的集成。 可以使用灵活的插件方法来扩展RabbitMQ功能。
(4) 易于监控,可以方便的使用HTTP-API,命令行工具或其他UI工具来管理和监控RabbitMQ。
不同的消息队列服务器有不同的应用场景,具体选择哪一款Mq,需要根据实际的业务场景来综合分析。在本文接下来中,会详细地说明RabbitMq的内在原理、高级特性以及基本的使用方法。
系统架构:
消息队列的使用过程大概如下:
如上图所示:AMQP 里主要说两个组件,Exchange 和 Queue。绿色的X
就是 Exchange ,红色的是 Queue ,这两者都在 Server 端,又称作 Broker,这部分是 RabbitMQ 实现的,而蓝色的则是客户端,通常有 Producer 和 Consumer 两种类型。
1.点对点队列:最简单的模型,一个生产者P发送消息到队列Q,一个消费者C接收
2.工作队列模型:一个生产者发送消息到队列中,有多个消费者共享一个队列,每个消费者获取的消息是唯一的。
为了保证服务器同一时刻只发送一条消息给消费者,保证资源的合理利用。channal.basicQos(1);这样是为了保证多个消费者接收的消息数量不一样,能者多劳,如果不设置,那么消费者是平均分配消息(例如10条消息,每个消费者接收5条)
RabbitMQ 是建立在强大的 Erlang OTP 平台上,因此安装 RabbitMQ 之前要先安装 Erlang.
1.安装Erlang
wget https://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm # 添加源
sudo rpm -Uvh erlang-solutions-1.0-1.noarch.rpm #执行时报错了,跳过即可。
sudo yum install erlang
2.安装RabbitMQ:
wget http://www.rabbitmq.com/releases/rabbitmq-server/v3.6.6/rabbitmq-server-3.6.6-1.el7.noarch.rpm
yum install rabbitmq-server-3.6.6-1.el7.noarch.rpm #下载完成后安装rabbitMQ
service rabbitmq-server start #启动rabbitmq服务
service rabbitmq-server status
service rabbitmq-server stop (干净的关闭)
这里只介绍最简单的单节点命令,关于集群中远程节点启停命令后续会介绍。
//Rabbitmq命令操作:
$ chkconfig rabbitmq-server on // 添加开机启动RabbitMQ服务
$ service rabbitmq-server start // 启动服务
$ service rabbitmq-server status // 查看服务状态
$ service rabbitmq-server stop // 停止服务,停止了整个rabbitmq节点(应用程序和Erlang节点)
// 查看当前所有用户
$ rabbitmqctl list_users
// 查看默认guest用户的权限
$ rabbitmqctl list_user_permissions guest
// 由于RabbitMQ默认的账号用户名和密码都是guest。任何引用该用户的访问控制条目都会从rabbit权限数据库中删除
$ rabbitmqctl delete_user guest
// 添加新用户
$ rabbitmqctl add_user username password
// 设置用户tag
$ rabbitmqctl set_user_tags username administrator
//赋予用户默认vhost的全部操作权限
// -p / 指定vhost
//username 被赋予权限的用户
//".*" ".*" ".*" 表示配置权限、写权限、读权限。匹配任何队列或者交换器的名字
$ rabbitmqctl set_permissions -p / username ".*" ".*" ".*"
//移除权限
$ rabbitmqctl set_permissions -p / username
//修改用户密码,指定用户名和新密码即可
$ rabbitmqctl change_password username new_password
// 查看用户的权限
$ rabbitmqctl list_user_permissions username
此处创建了用户,admin/admin,设置为administrator,并赋予全部操作权限。如下:
创建vhost,然后连接上去创建队列和交换器
[root@VM_0_3_centos software]# rabbitmqctl add_vhost host1
Creating vhost "host1" ...
[root@VM_0_3_centos software]# rabbitmqctl add_vhost host2
Creating vhost "host2" ...
[root@VM_0_3_centos software]# rabbitmqctl add_vhost host3
Creating vhost "host3" ...
[root@VM_0_3_centos software]# rabbitmqctl delete_vhost host3
Deleting vhost "host3" ...
[root@VM_0_3_centos software]# rabbitmqctl list_vhosts #列出所有的host
Listing vhosts ...
host1
/
host2
使用浏览器打开http://localhost:15672
访问 RabbitMQ 的管理控制台,使用刚才创建的账号admin/admin登陆系统即可。RabbitMQ 管理后台,可以更好的可视化方式查看 RabbitMQ 服务器实例的状态。
备注:
1)默认情况下,RabbitMQ的默认的guest用户只允许本机访问, 如果想让guest用户能够远程访问的话,只需要将配置文件中的loopback_users列表置为空即可,如下:{loopback_users, []}
2)记得要开放5672和15672端口,监听的端口号如下:
首先在POM文件中引入rabbitmq依赖
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>3.6.6version>
dependency>
然后编写连接工具类,创建连接工厂ConnectionFactory,设置服务器地址、端口号5672、用户名、密码、virtual host,从连接工厂中获取连接connection。
package testRabbitmq;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:43
* @description: rabbitmq连接类
*/
public class RabbitConnectionUtil {
public static Connection getConnection() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("host1");
//通过工厂获取连接
Connection connection = factory.newConnection();
return connection;
}
}
生产者实现思路如下
package testRabbitmq.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description: rabbit生产者
*/
public class TestRabbitProducer1 {
public static void main(String[] args) throws Exception {
/*
* 1)获取rabbit服务器连接
* 2)创建信道
* 3)声明队列,不存在就新建
* 4)创建消息
* 5)发布消息
* 6)关闭信道
* 7)关闭连接
* */
produceQueue();
}
/**
* 生产者将消息发送给队列
* @author [email protected]
* @date 2018/8/21
* @param
* @return
* @throws
*/
public static void produceQueue()throws Exception{
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
/*
* 声明队列,不存在就新建
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
durable:表示建立的消息队列是否是持久化(RabbitMQ重启后仍然存在,并不是指消息的持久化
exclusive :表示建立的消息队列是否只适用于当前TCP连接
autoDelete:表示当队列不再被使用时,RabbitMQ是否可以自动删除这个队列
arguments:定义了队列的一些参数信息,主要用于Headers Exchange进行消息匹配时
* */
channel.queueDeclare("queue1",false,false,false,null);
for(int i =0;i<20;i++){
String msg ="Hello World " + i;
channel.basicPublish("","queue1",null,msg.getBytes());
System.out.println("生产消息:"+msg);
}
channel.close();
connection.close();
}
}
消费者实现思路
获取rabbitmq服务器连接;创建信道channel;声明队列; 创建消费者并监听队列,从队列中读取消息。
package testRabbitmq.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer11 {
private final static String QUEUE_NAME = "queue1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
工作队列模型和点对点队列类似,区别是工作队列模型会有多个消费者同时从一个队列中消费消息。一个消息只能被一个消费者获取。
RabbitMQ可以设置basicQoS(Consumer prefetchCount)来对consumer进行流控,从而限制未ack的消息数量。 默认的prefetchCount是1。 prefetchCount值越大,获得的消息越多,可以理解为接收消息所占的比例。
消费者1:
package testRabbitmq.work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer21 {
private final static String QUEUE_NAME = "queue1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(3);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者1:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
package testRabbitmq.work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer22 {
private final static String QUEUE_NAME = "queue1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者2:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
结果:注释掉channel.basicQos(1) 消息会被平均分配给多个消费者
AMQP 协议中的核心思想就是:生产者和消费者隔离,生产者从不直接将消息发送给队列。生产者通常不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。先由 Exchange 来接收,然后 Exchange 按照特定的策略转发到 Queue 进行存储。同理,消费者也是如此。Exchange 就类似于一个交换机,转发各个消息分发到相应的队列中。
RabbitMQ 提供了四种 Exchange 模式:fanout、direct、topic、header. 由于 header 模式在实际使用中较少,因此本节只对前三种模式进行比较。
所有发送到 Fanout Exchange 的消息都会被转发到与该 Exchange 绑定(Binding)的所有 Queue 上。Fanout Exchange 不需要处理 RouteKey,只需要简单的将队列绑定到 exchange 上,这样发送到 exchange 的消息都会被转发到与该交换机绑定的所有队列上。**类似子网广播,每台子网内的主机都获得了一份复制的消息。**所以,Fanout Exchange 转发消息是最快的。
生产者:和前边两种模式不同的是,订阅模式需要定义exchange,并指定exchange的类型,此处为fanout
channel.exchangeDeclare("exchange_fanout","fanout",true);
package testRabbitmq.publishSubscribe;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description: Publish/Subscribe. 订阅模式,消息被路由投递给多个队列,一个消息被多个消费者获取。ExchangeType为fanout。
*/
public class TestRabbitProducer3 {
public static void main(String[] args) throws Exception {
produceExchange();
}
/**
* 生产者将消息发送给交换器
* @author [email protected]
* @date 2018/8/21
* @param
* @return
* @throws
*/
public static void produceExchange()throws Exception{
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_fanout","fanout",true);
for(int i =0;i<20;i++){
String msg ="Hello World " + i;
channel.basicPublish("exchange_fanout","",null,msg.getBytes());
System.out.println("生产消息:"+msg);
}
channel.close();
connection.close();
}
}
消费者1:需要绑定队列到交换器
package testRabbitmq.publishSubscribe;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer31 {
private final static String QUEUE_NAME = "queue_fanout_1";
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
consumeQueue();
}
public static void consumeQueue()throws Exception{
/*
* 1)获取rabbit服务器连接
* 2)创建信道
* 3)声明队列,不存在就新建
* 4)定义队列的消费者
* 5)监听队列
* 6)获取消息
* */
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 绑定队列到交换器. 绑定也可在rabbitMQ的管理界面进行
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
QueueingConsumer consumer = new QueueingConsumer(channel);
//true 自动确认消息 false为手动确认消息
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者1:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
package testRabbitmq.publishSubscribe;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer32 {
private final static String QUEUE_NAME = "queue_fanout_2";
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者2:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
所有发送到 Direct Exchange 的消息被转发到 RouteKey 中指定的 Queue.Direct 模式可以使用 RabbitMQ 自带的 Exchange:default Exchange ,因此不需要将 Exchange 进行任何绑定(binding)操作 。消息传递时,RouteKey 必须完全匹配,才会被队列接收,否则该消息会被抛弃。
生产者:发布消息时指定第二个参数routingkey channel.basicPublish("exchange_routing","0",null,msg.getBytes());
package testRabbitmq.routing;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description: 路由模式,一个消息被多个消费者获取。并且消息的目的queue可被生产者指定。ExchangeType为direct。
*/
public class TestRabbitProducer4 {
public static void main(String[] args) throws Exception {
produceExchange();
}
/**
* 生产者将消息发送给交换器
* @author [email protected]
* @date 2018/8/21
* @param
* @return
* @throws
*/
public static void produceExchange()throws Exception{
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_routing","direct",true);
for(int i =0;i<20;i++){
String msg ="Hello World " + i;
//第二个参数 是routingKey
if(i%3 == 0){
channel.basicPublish("exchange_routing","0",null,msg.getBytes());
System.out.println("生产消息,routingKey=0:"+msg);
}
if(i%3 == 1){
channel.basicPublish("exchange_routing","1",null,msg.getBytes());
System.out.println("生产消息,routingKey=1:"+msg);
}
if(i%3 == 2){
channel.basicPublish("exchange_routing","2",null,msg.getBytes());
System.out.println("生产消息,routingKey=2:"+msg);
}
}
channel.close();
connection.close();
}
}
消费者1:将队列绑定到exchange中,并指定routingkey channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"0");
package testRabbitmq.routing;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer41 {
private final static String QUEUE_NAME = "queue_routing_1";
//必须要提前创建好exchange
private final static String EXCHANGE_NAME = "exchange_routing";
public static void main(String[] args) throws Exception {
consumeQueue();
}
public static void consumeQueue()throws Exception{
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//将队列绑定到exchange中,并指定routingkey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"0");
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者1,routingKey=0:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
package testRabbitmq.routing;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer42 {
private final static String QUEUE_NAME = "queue_routing_2";
private final static String EXCHANGE_NAME = "exchange_routing";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"1");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"2");
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者2,routingKey=1|2:"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
所有发送到 Topic Exchange 的消息被转发到所有关心 RouteKey 中指定 Topic 的 Queue 上,Exchange 将 RouteKey 和某 Topic 进行模糊匹配。此时队列需要绑定一个 Topic,可以使用通配符进行模糊匹配,符号#
匹配一个或多个词,符号*
匹配一个词。因此log.#
能够匹配到log.info.oa
,但是log.*
只会匹配到log.error
.所以,Topic Exchange 使用非常灵活。
生产者:
package testRabbitmq.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description: Topic. 通配符模式,一个消息被多个消费者获取。消息的目的queue可用BindingKey以通配符(#:一个或多个词,*:一个词)的方式指定。ExchangeType为topic。
*/
public class TestRabbitProducer5 {
public static void main(String[] args) throws Exception {
produceExchange();
}
/**
* 生产者将消息发送给交换器
* @author [email protected]
* @date 2018/8/21
* @param
* @return
* @throws
*/
public static void produceExchange()throws Exception{
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("exchange_topic","topic",true);
for(int i =0;i<20;i++){
String msg ="Hello World " + i;
//topic.0 是routingKey
if(i%3 == 0){
channel.basicPublish("exchange_topic","topic.0",null,msg.getBytes());
System.out.println("生产消息,topic=0:"+msg);
}
if(i%3 == 1){
channel.basicPublish("exchange_topic","topic.1",null,msg.getBytes());
System.out.println("生产消息,topic=1:"+msg);
}
if(i%3 == 2){
channel.basicPublish("exchange_topic","topic.2",null,msg.getBytes());
System.out.println("生产消息,topic=2:"+msg);
}
}
channel.close();
connection.close();
}
}
消费者1:只能接收topic为topic.0
的消息
package testRabbitmq.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer51 {
private final static String QUEUE_NAME = "queue_topic_1";
//必须要提前创建好exchange
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) throws Exception {
consumeQueue();
}
public static void consumeQueue()throws Exception{
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"topic.0");
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者1,topic.0 :"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:topic.*
可以接收所有的消息
package testRabbitmq.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import testRabbitmq.RabbitConnectionUtil;
/**
* @author: Chen Xiuhong
* @date: 2018/8/21 09:51
* @description:
*/
public class TestRabbitConsumer52 {
private final static String QUEUE_NAME = "queue_topic_2";
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"topic.*");
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(QUEUE_NAME,false,consumer);
while (true){
//阻塞或轮询
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费者2,topic.* :"+msg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
在 Rabbitmq 中我们可以通过持久化来解决因为服务器异常而导致丢失的问题,
除此之外我们还会遇到一个问题:生产者将消息发送出去之后,消息到底有没有正确到达 Rabbit 服务器呢?如果不错
得数处理,我们是不知道的,(即 Rabbit 服务器不会反馈任何消息给生产者),也就是默认的情况下是不知道消息有没有
正确到达;
导致 的问题:消息到达服务器之前丢失,那么持久化也不能解决此问题,因为消息根本就没有到达 Rabbit 服务器!
RabbitMQ 为我们 提供了两种方式:
RabbitMQ 中与事务机制有关的方法有三个:txSelect(), txCommit()以及 txRollback(), txSelect 用于将当前 channel 设置成 transaction 模式,txCommit 用于提交事务,txRollback 用于回滚事务,在通过 txSelect 开启事务之后,我们便可以发布消息给 broker 代理服务器了,如果 txCommit 提交成功了,则消息一定到达了 broker 了,如果在 txCommit执行之前 broker 异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback 回滚事务了。
但是此种模式还是很耗时的,采用这种方式 降低了 Rabbitmq 的消息吞吐量
try {
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
int result = 1 / 0;
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
System.out.println("----msg rollabck ");
}
producer 端 confirm 模式的实现原理:
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 deliver-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继
续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果
RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处
理该 nack 消息。
备注:已经在 transaction 事务模式的 channel 是不能再设置成 confirm 模式的,即这两种模式是不能共存的。
//生产者通过调用channel的confirmSelect方法将channel设置为confirm模式
channel.confirmSelect();
channel.basicPublish("", QUEUE_NAME, null,msg.getBytes());
if(!channel.waitForConfirms()){
System.out.println("send message failed.");
}else{
System.out.println(" send messgae ok ...");
}
下边介绍RabbitMQ集群
rabbitmq内建集群儿都设计用于完成两个目标:允许消费者和生产者在rabbit节点崩溃的情况下继续允许,以及通过添加更多的节点来线性扩展消息通信吞吐量。rabbitmq通过利用Erlang提供的开放电信平台分布式通信框架来巧妙地满足以上两个需求。你可以失去一个rabbit节点,同时客户端能够重新连接到集群中的任何其他节点并继续生产或者消费消息。同样,如果rabbit集群正疲于应对庞大的消息通信,那么添加更多的节点会线性增加更多性能。但是当一个rabbit节点崩溃时,该节点上队列的消息也会丢失,这是因为rabbitmq默认不会将队列上的内容复制到集群中。所以需要我们进行特别配置。
rabbitmq是依据erlang的分布式特性(RabbitMQ底层是通过Erlang架构来实现的,所以rabbitmqctl会启动Erlang节点,并基于Erlang节点来使用Erlang系统连接RabbitMQ节点,在连接过程中需要正确的Erlang Cookie和节点名称,Erlang节点通过交换Erlang Cookie以获得认证)来实现的,所以部署rabbitmq分布式集群时要先安装erlang,并把其中一个服务的cookie复制到另外的节点。
RabbitMQ会始终记录以下四种类型的元数据:
RabbitMQ集群除了记录以上四种元数据外,还会记录新的元数据类型:
集群中创建队列时,只会在单个节点而不是全部节点上创建完整的队列消息(元数据、状态、内容)。其他非所有者节点只知道队列的元数据和指向该队列存在的那个节点的指针。
RabbitMQ的集群节点包括内存节点、磁盘节点。顾名思义内存节点就是将所有数据放在内存,磁盘节点将数据放在磁盘。不过,如果在投递消息时,打开了消息的持久化,那么即使是内存节点,数据还是会放在磁盘。原则上一个集群至少有一个磁盘节点。在实际使用中会发现所谓的磁盘节点是只用来存储集群的配置信息,也就是说如果集群中没有磁盘节点,当所有节点关机后集群的配置信息就会丢失。在进行性能测试时两个模式的节点订阅发布消息的性能没有太大差距。
单节点系统只允许磁盘类型的节点,否则每次重启rabbitmq之后,所有关于系统配置的信息都会丢失。而在集群中可以选择部分节点为内存节点,至少有一个节点为磁盘节点。
当节点加入或者离开集群时,必须将变更通知到至少一个磁盘节点。如果凑巧集群中仅有的一个磁盘节点down掉了,集群是可以继续路由消息的,但是不能创建交换器队列等元数据。所以最好是在一个集群中设置两个磁盘节点。
//修改节点为内存节点
./rabbitmqctl -n rabbit3@VM_0_3_centos change_cluster_node_type ram
Rabbit模式大概分为以下三种:单主机模式、普通集群模式、镜像集群模式。
正常情况下需要多台服务器来部署集群,鉴于服务器资源不足,此处使用一台服务器开启多个端口来配置rabbitmq集群。
rabbitmq启动Erlang节点和Rabbitmq应用程序很简单,找到
./sbin
目录,运行./rabbitmq-server
即可。启动的错误日志hi也可以到/var/log/rabbitmq/目录下查看。
可以通过增加
-detached
方式启动rabbitmq节点。
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.6/sbin
首先确保现存的rabbit没有运行,service rabbitmq-server stop
指定远程节点 -n rabbit@hostname
//节点1 端口号5672
RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit@VM_0_3_centos ./rabbitmq-server
//节点2 端口号5673
RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}] -rabbitmq_stomp tcp_listeners [61614] -rabbitmq_mqtt tcp_listeners [1884]" RABBITMQ_NODENAME=rabbit2 ./rabbitmq-server -detached
//节点3 端口号5674
RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}] -rabbitmq_stomp tcp_listeners [61615] -rabbitmq_mqtt tcp_listeners [1885]" RABBITMQ_NODENAME=rabbit3 ./rabbitmq-server -detached
这样仅仅是把需要的服务的节点启动了,但是各个服务之间并不知道彼此的存在,各个节点仍然是独立的几点,拥有自己的元数据。集群中的第一个节点会将元数据带入集群,并且无需被告知加入了,但是剩下了两个节点rabbit2和rabbit3必须加入先关闭其应用程序并清空其本身的元数据才能正式加入到集群当中来。所以,我们需要做如下的操作:
//停止第二个节点的应用程序(和stop命令不同,此处只停止了应用程序,Erlang节点还在运行着)
./rabbitmqctl -n rabbit2@VM_0_3_centos stop_app
//重新设置第二个节点的元数据和状态为清空状态。
./rabbitmqctl -n rabbit2@VM_0_3_centos reset
//加入第一节点
./rabbitmqctl -n rabbit2@VM_0_3_centos join_cluster rabbit1@VM_0_3_centos
//重新启动第二节点
./rabbitmqctl -n rabbit2@VM_0_3_centos start_app
//停止第三个节点的应用程序
./rabbitmqctl -n rabbit3@VM_0_3_centos stop_app
//重新设置第三个节点的元数据和状态为清空状态。
./rabbitmqctl -n rabbit3@VM_0_3_centos reset
//加入第一节点
./rabbitmqctl -n rabbit3@VM_0_3_centos join_cluster rabbit1@VM_0_3_centos
//重新启动第三节点
./rabbitmqctl -n rabbit3@VM_0_3_centos start_app
查看集群状态:
./rabbitmqctl cluster_status -n rabbit1@VM_0_3_centos
集群中创建用户admin/admin
# ./rabbitmqctl -n rabbit1@VM_0_3_centos add_user admin admin
Creating user "admin" ...
# ./rabbitmqctl -n rabbit1@VM_0_3_centos set_user_tags admin administrator
Setting tags for user "admin" to [administrator] ...
# ./rabbitmqctl -n rabbit1@VM_0_3_centos set_permissions -p / admin ".*" ".*" ".*"
Setting permissions for user "admin" in vhost "/" ...
集群情况如下:三个节点都是磁盘节点
我们可以通过命令修改节点为内存节点
./rabbitmqctl -n rabbit3@VM_0_3_centos stop_app
./rabbitmqctl -n rabbit3@VM_0_3_centos change_cluster_node_type ram
./rabbitmqctl -n rabbit3@VM_0_3_centos start_app
此次部署是在多台机器之间部署rabbitmq的cluster,要求如下:这几个节点需要再同一个局域网内;这几个节点需要有相同的erlang cookie,否则不能正常通信,为了实现cookie内容一致,采用scp的方式进行。
1)分别在3台机器上安装erLang和rabbitmq ,安装步骤参照第三部分。
2)设置erlang
找到erlang cookie文件的位置,官方在介绍集群的文档中提到过.erlang.cookie一般会存在这两个地址:第一个是$home/.erlang.cookie
;第二个地方就是/var/lib/rabbitmq/.erlang.cookie
。如果我们使用解压缩方式安装部署的rabbitmq,那么这个文件会在${home}
目录下,也就是$home/.erlang.cookie
。如果我们使用rpm等安装包方式进行安装的,那么这个文件会在/var/lib/rabbitmq
目录下。
然后将 node1 的该文件复制到 node2、node3。查看三台机器的cookie是否一致,设置erlang的目的是要保证集群内的cookie内容一致。
3)运行各节点
service rabbitmq-server start ,此时每个节点是作为单独的一台RabbitMQ存在的,可以正常提供服务。
4)组成集群
rabbitmq-server启动时,会一起启动节点和应用,它预先设置RabbitMQ应用为standalone模式。要将一个节点加入到现有的集群中,你需要停止这个应用,并将节点设置为原始状态。使用service rabbitmq-server stop
,应用和节点都将被关闭。所以使用rabbitmqctl stop_app仅仅关闭应用。
将 node2、node3与 node1 组成集群,这里以node2为例
node2# rabbitmqctl stop_app
node2# rabbitmqctl join_cluster rabbit@node1 ####这里集群的名字一定不要写错了
node2# rabbitmqctl start_app
将node3重复上述操作,也加入node1的集群。 集群配置好后,可以在 RabbitMQ 任意节点上执行 rabbitmqctl cluster_status
来查看是否集群配置成功。
5)设置镜像队列策略
rabbitmqctl set_policy -p host1 ha-all "^" '{"ha-mode":"all"}'
“host1” vhost名称, "^"匹配所有的队列, ha-all 策略名称为ha-all, ‘{“ha-mode”:“all”}’ 策略模式为 all 即复制到所有节点,包含新增节点。
则此时镜像队列设置成功。(这里的虚拟主机host1是代码中需要用到的虚拟主机,虚拟主机的作用是做一个消息的隔离,本质上可认为是一个rabbitmq-server,是否增加虚拟主机,增加几个,这是由开发中的业务决定,即有哪几类服务,哪些服务用哪一个虚拟主机,这是一个规划)。
rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
-p Vhost: 可选参数,针对指定vhost下的queue进行设置
Name: policy的名称
Pattern: queue的匹配模式(正则表达式)
Definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode
ha-mode:指明镜像队列的模式,有效值为 all/exactly/nodes
all:表示在集群中所有的节点上进行镜像
exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定
ha-params:ha-mode模式需要用到的参数
ha-sync-mode:进行队列中消息的同步方式,有效值为automatic和manual
priority:可选参数,policy的优先级
将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态保持一直。完成这 6 个步骤后,RabbitMQ 高可用集群就已经搭建好了,最后一个步骤就是搭建均衡器。
若想让集群规模更小,或者用更好的硬件来替换其中一个节点时,需要让节点离开集群。
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl reset命令是清空节点状态,并将其恢复到空白状态。当重设的节点是集群的一部分时,该命令会和集群中的磁盘节点通信,告诉他们该节点正在离开集群。否则集群会认为该节点出现故障,并期望能修复故障回到集群中,然后才允许新节点加入。因此,简单的把磁盘节点从集群中猛拉出来而非正式移除的话,会导致集群永久性无法变更。所以从集群中移除节点时,需要小心重设节点状态。
如下图,将rabbit3节点从集群中移除,然后查询rabbit1集群状态和rabbit3状态
(1)HAProxy 是一款提供高可用性、负载均衡以及基于TCP(第四层)和HTTP(第七层)应用的代理软件,支持虚拟主机,它是免费、快速并且可靠的一种解决方案。 HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会话保持或七层处理。HAProxy运行在时下的硬件上,完全可以支持数以万计的 并发连接。并且它的运行模式使得它可以很简单安全的整合进您当前的架构中, 同时可以保护你的web服务器不被暴露到网络上。
(2)HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。多进程或多线程模型受内存限制 、系统调度器限制以及无处不在的锁限制,很少能处理数千并发连接。事件驱动模型因为在有更好的资源和时间管理的用户端(User-Space) 实现所有这些任务,所以没有这些问题。此模型的弊端是,在多核系统上,这些程序通常扩展性较差。这就是为什么他们必须进行优化以 使每个CPU时间片(Cycle)做更多的工作。
(3)HAProxy 支持连接拒绝 : 因为维护一个连接的打开的开销是很低的,有时我们很需要限制攻击蠕虫(attack bots),也就是说限制它们的连接打开从而限制它们的危害。 这个已经为一个陷于小型DDoS攻击的网站开发了而且已经拯救
了很多站点,这个优点也是其它负载均衡器没有的。
(4)HAProxy 支持全透明代理(已具备硬件防火墙的典型特点): 可以用客户端IP地址或者任何其他地址来连接后端服务器. 这个特性仅在Linux 2.4/2.6内核打了cttproxy补丁后才可以使用. 这个特性也使得为某特殊服务器处理部分流量同时又不修改服务器的地址成为可能。
yum Install haproxy #HAProxy目前主要有三个版本: 1.3 , 1.4 ,1.5,CentOS6.6 自带的RPM包为 1.5 的。
[root@VM_0_3_centos ~]# rpm -qi haproxy
Name : haproxy
Version : 1.5.18
Release : 7.el7
Architecture: x86_64
Install Date: Wed 19 Sep 2018 09:17:03 PM CST
Group : System Environment/Daemons
Size : 2689838
License : GPLv2+
Signature : RSA/SHA256, Wed 25 Apr 2018 07:04:31 PM CST, Key ID 24c6a8a7f4a80eb5
Source RPM : haproxy-1.5.18-7.el7.src.rpm
Build Date : Wed 11 Apr 2018 12:28:42 PM CST
Build Host : x86-01.bsys.centos.org
Relocations : (not relocatable)
Packager : CentOS BuildSystem
Vendor : CentOS
URL : http://www.haproxy.org/
Summary : TCP/HTTP proxy and load balancer for high availability environments
Description :
HAProxy is a TCP/HTTP reverse proxy which is particularly suited for high
availability environments. Indeed, it can:
- route HTTP requests depending on statically assigned cookies
- spread load among several servers while assuring server persistence
through the use of HTTP cookies
- switch to backup servers in the event a main server fails
- accept connections to special ports dedicated to service monitoring
- stop accepting connections without breaking existing ones
- add, modify, and delete HTTP headers in both directions
- block requests matching particular patterns
- report detailed status to authenticated users from a URI
intercepted by the application
[root@VM_0_3_centos ~]# rpm -ql haproxy
/etc/haproxy
/etc/haproxy/haproxy.cfg
/etc/logrotate.d/haproxy
/etc/sysconfig/haproxy
/usr/bin/halog
/usr/bin/iprange
/usr/lib/systemd/system/haproxy.service
/usr/sbin/haproxy
/usr/sbin/haproxy-systemd-wrapper
/usr/share/doc/haproxy-1.5.18
/usr/share/doc/haproxy-1.5.18/CHANGELOG
/usr/share/doc/haproxy-1.5.18/LICENSE
/usr/share/doc/haproxy-1.5.18/README
vim/etc/haproxy/haproxy.cfg
[root@VM_0_3_centos ~]# vim /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Example configuration for a possible web application. See the
# full configuration options online.
#
# http://haproxy.1wt.eu/download/1.4/doc/configuration.txt
#
#---------------------------------------------------------------------
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
# to have these messages end up in /var/log/haproxy.log you will
# need to:
#
# 1) configure syslog to accept network log events. This is done
# by adding the '-r' option to the SYSLOGD_OPTIONS in
# /etc/sysconfig/syslog
#
# 2) configure local2 events to go to the /var/log/haproxy.log
# file. A line like the following can be added to
# /etc/sysconfig/syslog
#
# local2.* /var/log/haproxy.log
#
log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
daemon
# turn on stats unix socket
stats socket /var/lib/haproxy/stats
#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
mode tcp
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
#为指定的frontend定义其最大并发连接数;默认为3000;
maxconn 3000
#---------------------------------------------------------------------
# main frontend which proxys to the backends
#---------------------------------------------------------------------
listen rabbitmq_local_cluster 127.0.0.1:5671
mode http
balance roundrobin
server rabbit1 127.0.0.1:5672 check inter 5000 rise 2 fall 3 weight 1
server rabbit2 127.0.0.1:5673 check inter 5000 rise 2 fall 3 weight 1
server rabbit3 127.0.0.1:5674 check inter 5000 rise 2 fall 3 weight 1
listen monitor :8100
mode http
option httplog
stats enable
stats uri /stats
stats refresh 5s
运行haproxy -f haproxy.cfg 即可。
通过设置的haproxy的监控地址端口8100,可以通过浏览器进行查看数据统计界面,如图5.2haproxy数据统计界面所示。其中显示的绿色的背景的三个节点rabbit1以及rabbit2 rabbit3代表可用状态。
访问http://118.25.175.161:8100/stats
比如12306订票有两个程序,分别是前端订单接收器和订单处理器。现在每秒接收100W条订单,前端订单接收器可以跟得上负载,但是订单处理器一直在处理订单,客户会一直等待订单处理结果。所以这时候就需要更多的订单处理器。这就是接下来考虑的可扩展性,无需更改代码,灵活的附加新的订单处理器。
考虑到高可用性需要设置集群,rabbitmq自带的内建集群,如何快速升级集群、增加节点或者减少节点