MQ(Message Queue简称MQ)作为Linux进程间八种(管道、命名管道、内存映射、消息队列、共享内存、信号量、信号、套接字)通信方式之一,它基于AMQP(Advanced Message Queuing Protocol)协议,实现了相互隔离的进程间的信息通信。主流的MQ框架有RabbitMQ、ActiveMQ、ZeroMq、kafka,以及阿里开源的RocketMQ,其中使用最为广泛的当属RabbitMQ了。
RabbitMQ使用Erlang(面向并发的一门编程)语言开发
RabbitMQ的安装需要依赖Erlang,所以需要先安装Erlang环境
RabbitMQ 3.7.7 windows安装文件https://pan.baidu.com/s/1KIceNVkAWU9NPNiEYGsqXw 提取码:xwg9
参照文档安装即可
Linux 版本和MacOX版本安装文件在官网下载安装即可,切记Erlang的版本要和RabbitMQ版本对应
总的来说,RabbitMQ队列模式分为两大类,使用交换机与不使用交换机两大类;其中不使用交换机的有两种,一对一与一对多,使用交换机的有4种,Direct,Fanout,Topic,Headers;使用交换机时,生产者的消息不直接发送到消费者队列,而是先发送到交换机,再由交换机根据绑定的路由关系路由到消费者监听的队列;此外还有一种默认交换机,它实际上是一个由消息代理预先申明好的没有名字(名字为默认空字符串)的直连交换机,它使得每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。
RabbitMQ是基于AMQP协议的,所以协议规定的相关规范RabbitMQ都有实现,还做了一些自己的扩展。
AMQP模型
Queue:生产者、消费者通过监听队列获取队列中的消息,队列在声明后才能被使用。如果一个队列不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为 406 的通道级异常就会被抛出。
Exahange:交换机,不存储消息,负责将生产者生产的消息,按照绑定的路由键路由到指定的消费者队列。
RoutingKey:路由键,用来将队列和交换机进行绑定,生产者发送消息时,可以通过指定路由键让交换机将消息路由至指定的队列。
Vhost:类似于一个命名空间,Queue,Exchange都归属于Vhost,不同的Vhost中Queue,Exchange不同
RabbitMQ目前支持Java、Python、Ruby、Go、PHP、C#等多种语言,Java可单独使用RabbitMQ,也可通过Springboot集成
(1)Java使用原生RabbitMQ
引入依赖
com.rabbitmq amqp-client 5.4.3
获取连接工具类
package com.twp.spring.utils;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class RabbitConnectionFactory {
public static Connection getConnection(){
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("123456");
factory.setVirtualHost("/");
Connection connection = null;
try {
connection = factory.newConnection();
}catch (Exception error){
error.printStackTrace();
}
return connection;
}
}
生产者
package com.twp.spring.producer;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.twp.spring.utils.RabbitConnectionFactory;
public class ProducerTest {
private static final String QUEUE = "foo_queue";
private static final String EXCHANGE = "foo_exchange";
private static final String ROUTINGKEY = "foo_key";
public static void main(String[] args) {
Connection connection = RabbitConnectionFactory.getConnection();
if(connection != null){
try {
//获取Channel,所有关于队列的操作都将在Channel中进行
Channel channel = connection.createChannel();
//声明交换机的名字和类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers"),以及是否需要持久化,使用非交换机模式不需要进行交换机声明,
channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.DIRECT,true);
//声明队列,指定队列名,是否持久化(非持久化使用之后会自动删除),是否排他(排他只能当前连接使用),是否自动删除(生产者发送完消息后会自动删除),队列其他属性参数(Map)
channel.queueDeclare(QUEUE,true,false,false,null);
//将队列与交换机进行绑定
channel.queueBind(QUEUE,EXCHANGE,ROUTINGKEY);
String message = "Hello RabbitMQ";
channel.basicPublish(EXCHANGE,ROUTINGKEY,null,message.getBytes());
System.out.println("Producer 当前时间:" + System.currentTimeMillis());
System.out.println("已发送消息:" + message);
channel.close();
connection.close();
}catch (Exception error){
error.printStackTrace();
}
}
}
}
消费者
package com.twp.spring.consumer;
import com.rabbitmq.client.*;
import com.twp.spring.utils.RabbitConnectionFactory;
import java.io.IOException;
public class ConsumerTest {
private static final String QUEUE = "foo_queue";
public static void main(String[] args) {
Connection connection = RabbitConnectionFactory.getConnection();
if(connection != null){
try {
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE,true,false,false,null);
channel.basicConsume(QUEUE,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
System.out.println("Consumer 当前时间:" + System.currentTimeMillis());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("Message ContentType:" + properties.getContentType());
System.out.println("Message Content:" + (new String(body)));
}
});
}catch (Exception error){
error.printStackTrace();
}
}
}
}
启动消费者,等待生产者发送消息
RabbitMQ前端显示当前消费者已连接
启动生产者发送消息
至此就完成了生产者生产消息,消费者消费消息,整条链路,下面来看看Springboot集成RabbitMQ
(2)Springboot对RabbitMQ已经做了专门的封装,简化开发人员对RabbitMQ的调用,springboot使用RabbitMQ极其简单,只需简单几步就能使用RabbitMQ进行消息发送,接收。
引入Springboot AMQP starter,它会帮我们自动引入RabbitMQ的相关依赖,并自动注入我们需要的相关bean
org.springframework.boot spring-boot-starter-amqp
配置RabbitMQ连接信息
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: root
password: 123456
virtual-host: "/"
声明队列、交换机、绑定关系
package com.twp.spring.configuration;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static com.twp.spring.commons.RabbitConstants.*;
@Configuration
public class RabbitConfiguration {
@Bean
public Queue queue(){
//默认持久化,非排他,不自动删除
return new Queue(QUEUE);
}
@Bean
public Exchange exchange(){
//默认持久化,不自动删除
return new DirectExchange(EXCHANGE);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(exchange()).with(ROUTINGKEY).noargs();
}
}
生产者
package com.twp.spring.producer;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import static com.twp.spring.commons.RabbitConstants.*;
@Service
public class RabbitPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(String message){
rabbitTemplate.convertAndSend(EXCHANGE,ROUTINGKEY,message);
System.out.println("Send Success,message:" + message);
}
}
消费者
package com.twp.spring.consumer;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.twp.spring.commons.RabbitConstants.*;
@Component
@RabbitListener(queues = QUEUE)
public class RabbitConsumer {
/**
* @Payload 接收消息体(消息中的body内容)
* @Headers 接收消息头所有内容(Map),@Header接收单个消息头
* @RabbitHandler 指定监听队列的处理方法
* @RabbitListener(bindings = @QueueBinding(
* exchange = @Exchange(value="fant_exchange",type="fanout"),
* value = @Queue(value="foo.foo",durable="true"),
* key = "fant")
* )指明绑定关系,交换机等
*/
@RabbitHandler
public void handle(@Payload String message, @Headers Map headers){
System.out.println("Receive Message: " + message);
}
}
应用启动之后,通过自定义的http请求接口触发消息发送,监听队列的消费者就会实时消费队列中的消息
(1)防止消息丢失,消息丢失分为消费者丢失消息,RabbitMQ丢失消息,消费者丢失消息
消费者丢失消息处理方案:
开启RabbitMQ事务机制,会使RabbitMQ的性能下降10倍左右,一般不推荐使用
开启生产者消息确认(Confirm模式,异步),配置publisher-confirm-type指定确认方式,配置publisher-returns为true,设置RabbitTemplate.setMandatory(true),自定义消息确认回调类实现RabbitTemplate.ConfirmCallback(消息正确发送到Exchange后回调),实现RabbitTemplate.ReturnCallback接口(消息从Exchange路由到队列失败时回调)
RabbitMQ丢失消息处理方案:
设置消息持久化,Exchange,Queue,设置为持久化,Message持久化,消息deliveryMode=2
MQ采用集群镜像部署模式(队列、消息存在于多个节点上保证高可用)
消费者消息丢失处理方案:
开启消费者手动确认,自定义消息监听类实现ChannelAwareBatchMessageListener,处理消息消费确认、拒绝、重入队列,配置SimpleMessageListenerContainer,设置消息监听类,手动确认模式
使用第三方实现消息补偿
生产者发送消息之后同时异步将消息存在数据库,并设置状态,消费者成功消费消息之后,更改消息状态,消息补偿服务定期轮询消息状态或通过其他方式触发补偿服务,读取数据库中消费失败消息,重新将消息发往MQ
(2)防止消息重复消费
解决方案:保证消息的幂等性
为消息设置为一的ID,若消息需要入库,先通过唯一ID查询,存在不做处理
使用redis记录消息处理记录,处理消息前先查询,已处理则直接丢弃
业务处理逻辑本身是幂等的,如update,则无需处理