RabbitMQ
集群架构
-
主备模式
- 热备份,master->slave,当master挂掉以后启用slave为主节点,当master再次启动时,将成为从节点
-
主要配置
-
远程模式
-
数据异地容灾难,当单个节点处理不过来的时候,将数据转发到下游集群来处理
-
远距离通信和辅助,可以实现双活的一种模式
-
-
镜像模式
- 可以保证数据100%不被丢失
- 不支持横向扩展
-
多活模式
- 数据异地复制
- 异地容灾
- 数据多活
核心概念
通过普通协议在完全不同的应用之间实现数据共享,基于 c,不适合做大的消息堆积除非消费端足够大足够多
- AMQP 协议(Advanced Message Queuing Protocol,高级消息队列协议),是具有现代特征的二进制协议,是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间价设计
-
S ever:又称broker,接受客户端链接,实现AMQP实体服务器。
-
Channel:网络通道,机会所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端科技简历多个Channel,每个Channel代表一个会话任务
-
Message:消息,服务器和应用程序之间 传送的数据,由properties和body组成。properties可以对消息进行修饰,比如晓得优先级、延迟等高级特性;body则就是消息体内容。
-
Virtual host:虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个Virtual host里面不能有相同名称的exchange、Queue
-
exchange:交换机,接受消息,根据路由键转发消息到绑定的队列
-
Bingding:exchange和Queue之间的虚拟链接,Bingding中可以包含routing key
-
routing key:一个路由规则,虚拟机可以用它来确定如何路由一个特定消息
-
Queue:也称为Message Queue,消息队列,保存消息并将它们转发给消费者
安装
下载环境包
安装依赖包
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz -y
配置主机名称
vi /etc/hostname
lhy-151
vi /etc/hosts
192.168.4.151 lhy-151
安装程序包
rpm -ivh erlang-18.3.4.5-1.el7.centos.x86_64.rpm
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
修改配置
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
修改 {loopback_users, [<<“guest”>>]} 只保留{loopback_users, [“guest”]}
启动 使用 lsof -i:5672 查看端口是否被监听
/etc/init.d/rabbitmq-server start | stop | status | restart
安装管理插件 查看是否成功: lsof -i:15672
rabbitmq-plugins enable rabbitmq_management
访问
http://192.168.4.151:15672/
生产者消费者模型
生产者
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//ip
connectionFactory.setHost("192.168.0.106");
//端口
connectionFactory.setPort(5672);
//业务域(订单域、注册域。。。)类似mysql中的某个数据库
connectionFactory.setVirtualHost("/");
//创建链接
Connection connection = connectionFactory.newConnection();
String queryName = "test001";
Channel channel = connection.createChannel();
// 队列名称、是否持久化、独占队列、是否自动删除,队列的属性
channel.queueDeclare(queryName, false, false, false, null);
Map<String, Object> headers = new HashMap<>();
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
//消息持久化为2
.deliveryMode(2)
//字符集
.contentEncoding("UTF-8")
//header头
.headers(headers)
.build();
for (int i = 0; i < 5; i++) {
String msg="你好罗恒"+i;
//不指定exchange使用默认的exchange 按routingKey与queues名称匹配
channel.basicPublish("",queryName,properties,msg.getBytes());
}
消费者
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置链接ip
connectionFactory.setHost("192.168.0.106");
//设置链接端口
connectionFactory.setPort(5672);
//业务域(订单域、注册域。。。)类似mysql中的某个数据库
connectionFactory.setVirtualHost("/");
//创建链接
Connection connection = connectionFactory.newConnection();
String queryName = "test001";
//创建频道,使用频道与mq通信,多个频道可以复用connection,而不用每次都去销毁和创建链接
Channel channel = connection.createChannel();
//创建消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 设置频道的队列名称,是否自动ack(发送ack后消息会在队列中删除,重要的消息需要手动发送ack以确保消息被正确消费了),消费者(通过消费者获取消息)
channel.basicConsume(queryName,true,consumer);
while (true){
//获取队列中的消息,没有消息时会阻塞线程,
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//通过交付类获取消息的相信信息
byte[] body = delivery.getBody();
System.out.println(new String(body));
}
exchange交换机
接受消息,并根据路由键(Routing key )转发消息到所绑定的队列,属性如下:
-
name:交换机名称
-
type:交换机类型
-
direct
-
如果发送消息时不发送exchange名称,Routing key 与队列名称完全匹配才会被队列收到
-
如果发送消息时发送exchange名称,那么发送时消息时的 Routing key 与exchange和队列绑定时的 Routing key 完全匹配,该队列才会收到消息
-
消费者
//创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //ip connectionFactory.setHost("192.168.4.151"); //端口 connectionFactory.setPort(5672); //业务域(订单域、注册域。。。)类似mysql中的某个数据库 connectionFactory.setVirtualHost("/"); //创建链接 Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); //生名 String exChangeName="test_direct_exchange"; String exChangeType="direct"; String queueName="test_direct_queue"; String routingKey="test_direct_routingKey"; //创建一个 exchange channel.exchangeDeclare(exChangeName,exChangeType); //创建一个 queue channel.queueDeclare(queueName,true,false,false,null); //绑定 queue与exchange并设置routingKey channel.queueBind(queueName,exChangeName,routingKey); //创建消费者 QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queueName,true,consumer); while (true){ QueueingConsumer.Delivery delivery = consumer.nextDelivery(); String msg = new String(delivery.getBody()); System.out.println(msg); }
-
生产者
//创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //ip connectionFactory.setHost("192.168.4.151"); //端口 connectionFactory.setPort(5672); //业务域(订单域、注册域。。。)类似mysql中的某个数据库 connectionFactory.setVirtualHost("/"); //创建链接 Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); String exChangeName="test_direct_exchange"; String routingKey="test_direct_routingKey"; String msg = "罗恒一发来的消息"; //发送消息 指定 exChange和 routingKey channel.basicPublish(exChangeName,routingKey,null,msg.getBytes());
-
-
topic exchange将routeKey 和某个topic进行模糊匹配。此时队列需要绑定一个Topic类型的exchange
- 符号 “#” 匹配一个或多个例如:log.# 能够匹配到 “log.info.oa”
- 符号 “*” 只能匹配1个词 例如:
log.*
能偶匹配到 “log.erro”
-
fanout 广播模式,发送到交换机的消息会被转发到所有与该交换机绑定的队列上。效率最高
-
headers 消息头模式
-
-
Durabillity :是否持久化
-
Auto delete: 当exchange上绑定的队列全部删除后,自动删除该exchange
-
internal:当前exchange是否用于mq内部使用默认false
-
Arguments:扩展参数,用户扩展amqp协议自制定化使用
生产端可靠性投递与消费端幂等性
如何保证消息成功投递
对发送的消息进行记录并标记状态,等待mq的ack后标记为成功。期间使用定时任务扫描发送记录对超时的log进行重新发送
幂等性
保证消息在重复发送时不重复消费
(1)比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update一下好吧
(2)比如你是写redis,那没问题了,反正每次都是set,天然幂等性
(3)比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
confirm 确认机制
消息确认,是指生产者投递消息后,如果broker收到消息,则会给我们生产者一个应答,生产者进行接受应答,用来确认这条消息是否正常发送到 broker。这一过程是异步实现
如何实现confirm确认消息
-
第一步:在channel上开启确认模式:channel.confirmSlect()
-
第二部:在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送,或记录或记录日志等待后续处理!
//创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //ip connectionFactory.setHost("192.168.0.107"); //端口 connectionFactory.setPort(5672); //业务域(订单域、注册域。。。)类似mysql中的某个数据库 connectionFactory.setVirtualHost("/"); //创建链接 Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); String exChangeName="test_direct_exchange"; String exChangeType="direct"; String queueName="test_direct_queue"; String routingKey="test_direct_routingKey"; String msg="luohyengyi发送的"; //开启确认模式 channel.confirmSelect(); channel.addConfirmListener(new ConfirmListener() { @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("--------ok-----------"); } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { System.out.println("-----------error--------"); } }); channel.basicPublish(exChangeName,routingKey,null,msg.getBytes());
return 消息机制
retunrn Listener 用于处理一些不可路由的消息
消息生产者,通过指定一个exchange和routingkey。吧消息送达到某一个队列中去,然后我们的消费者监听队列,进行消费处理操作。但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key,路由不到,这个时候如果我们需要监听这个种不可达的消息,就要使用 return Listener!
在基础api中有一个关键的配置项
-
Mandatory :如果为true,则监听器会接受到路由不可到达的消息,然后进行后续处理,二u过为false,那么broker端自动删除该消息。
//创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //ip connectionFactory.setHost("192.168.0.107"); //端口 connectionFactory.setPort(5672); //业务域(订单域、注册域。。。)类似mysql中的某个数据库 connectionFactory.setVirtualHost("/"); //创建链接 Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); String exChangeName="test_direct_exchange"; String exChangeType="direct"; String queueName="test_direct_queue"; String routingKey="test_direct_routingKey123"; String msg="luohyengyi发送的"; //开启确认模式 channel.confirmSelect(); channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("*******************handleReturn*****"); System.out.println("replyCode:"+replyCode); System.out.println("replyText:"+replyText); System.out.println("exchange:"+exchange); System.out.println("routingKey:"+routingKey); System.out.println("body:"+new String(body)); } }); boolean mandatory= true; channel.basicPublish(exChangeName,routingKey,mandatory,null,msg.getBytes());
消费端限硫
RabbitMq 提供一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channek设置qos的值)未被确认前,不进行消费新的消息
-
void BasicQos(int prefetchSize,ushort prefetchCount,bool globle)
- prefetchSize 报文大小
- prefetchCount 提示mq不要同时给一个消费端推送多余N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack
- globle 是否将上面的设置应用于channel,true所有channel下的消费者都使用这个配置。fasle 只有这个消费者使用这个配置
ConnectionFactory connectionFactory = new ConnectionFactory(); //设置链接ip connectionFactory.setHost("192.168.4.29"); //设置链接端口 connectionFactory.setPort(5672); //业务域(订单域、注册域。。。)类似mysql中的某个数据库 connectionFactory.setVirtualHost("/"); //创建链接 Connection connection = connectionFactory.newConnection(); String queryName = "test001"; //创建频道,使用频道与mq通信,多个频道可以复用connection,而不用每次都去销毁和创建链接 Channel channel = connection.createChannel(); //创建消费者 QueueingConsumer consumer = new QueueingConsumer(channel); //同时只能消费1条消息 channel.basicQos(0,1,false); // 设置频道的队列名称,是否自动ack(发送ack后消息会在队列中删除,重要的消息需要手动发送ack以确保消息被正确消费了),消费者(通过消费者获取消息) channel.basicConsume(queryName,false,consumer); while (true){ //获取队列中的消息,没有消息时会阻塞线程, QueueingConsumer.Delivery delivery = consumer.nextDelivery(); //通过交付类获取消息的相信信息 byte[] body = delivery.getBody(); System.out.println(new String(body)); channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false); }
消费端ack与重回队列
消费成功通知ack消费失败nack,消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置链接ip
connectionFactory.setHost("192.168.4.29");
//设置链接端口
connectionFactory.setPort(5672);
//业务域(订单域、注册域。。。)类似mysql中的某个数据库
connectionFactory.setVirtualHost("/");
//创建链接
Connection connection = connectionFactory.newConnection();
String queryName = "test001";
//创建频道,使用频道与mq通信,多个频道可以复用connection,而不用每次都去销毁和创建链接
Channel channel = connection.createChannel();
//创建消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 设置频道的队列名称,是否自动ack(发送ack后消息会在队列中删除,重要的消息需要手动发送ack以确保消息被正确消费了),消费者(通过消费者获取消息)
channel.basicConsume(queryName,false,consumer);
while (true){
//获取队列中的消息,没有消息时会阻塞线程,
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//通过交付类获取消息的相信信息
byte[] body = delivery.getBody();
System.out.println(new String(body));
//手动ack,nack可以选择重回队列,回到队列的尾部
if ((Integer) delivery.getProperties().getHeaders().get("flag")==0){
channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,false);
}else {
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
}
TTL队列/消息
TTL 生存时间
-
TTL队列
队列中的消息都有统一的生存时间,超过时间消息会被删除
-
TTL消息
一条消息有自己的生存时间,这条消息在mq中的存活时间过期后会被删除,在发送消息时指定