RabbitMQ
消息队列目前常用的有很多种,比如ActiveMQ、Kafka、RocketMQ,甚至使用Redis简单实现一个消息队列。这里主要记录一下RabbitMQ的相关知识。
1、RabbitMQ的使用场景
2、RabbitMQ的缺点
3、RabbitMQ数据传递过程
4、RabbitMQ的Exchange类型
5、RabbitMQ使用实例
RabbitMQ的使用场景
消息队列的使用通常有三个重点:异步、削峰、解耦
1、当系统耦合较高时,不利于我们维护与扩展,例如,AB两个系统都需要C系统的数据,C系统通过接口调用的方式传递给AB,如果后期又有D系统需要,则要对C系统进行修改,这时可以使用消息队列,谁需要数据谁来消费即可。
2、当某个接口不需要实时返回时,允许出现短时间的不一致性时可以使用,这样可以降低响应时间,带来更好的体验,例如用户注册时,只需要往数据库中插入一条数据即可返回响应,而需要做的其他操作就可以发到队列里慢慢完成。
3、当一个接口的瞬时请求量非常巨大的时候,我们的系统可能根本承受不住,直接就挂掉了,这时候我们可以将请求全部发送到队列当中,让系统根据自己的能力来处理,一秒钟5000个请求进来,只有2000个交给系统来处理,剩下的挤压等待处理。
RabbitMQ的缺点
MQ的缺点在于,如何保证MQ可用性,数据的一致性,系统的复杂性。
1、现在都是通过搭建集群来保证MQ的可用性,一台挂掉了,不会影响系统的正常运行
2、数据一致性问题是最重要的,一条消息由多个消费者处理,其中一个消费者处理失败,会不会导致数据不一致。如果消费者处理消息后挂掉了,没有返回执行结果,下次重启会不会再消费一次?如果在MQ没有消费的时候,会不会有可能消息丢失?
3、如何保证消息是顺序消费的?比如一个消息队列有多个消费者?
这些常见的问题,我们会在文章的最后来解决。
RabbitMQ数据传递过程
图示消息传递过程(图片来源于网络)
从图片上可以看出,一条消息并不是直接发送到队列当中的,而是通过Exchange建立的通道channel,将消息首先发送到Exchange中,然后由Exchange定义的路由规则转发到消息队列Queue中,而消费者则通过channel从队列中获取消息。根据路由规则的不同选择不同的Exchange。
RabbitMQ的Exchange类型
Exchange在转发消息的时候会通过路由规则来进行匹配,如果没有匹配的队列,消息会被丢弃。
根据路由规则的不同,我们可以选用4个类型:
1、direct: Exchange在和Queue进行绑定Binding时会设定一个routing key,而消息生产者发送消息时会指定一个参数routing key,只有在完全匹配的情况下才会发送到队列当中。
2、topic: 与direct类似, Exchange在设置routing key时可以指定通配符 和 “#” ,其中“ ”匹配一个单词,“ # ”可以匹配多个单词或者无
3、fanout:这个路由规则就是直接匹配到所有绑定的队列,不需要routing key匹配,设置了也是无效的,通常用来做广播。
4、header:其路由的规则是根据header来判断,这种方式很少使用,了解即可
默认Defualt: 如果定义Exchange时没有指定类型和名称, RabbitMQ将会为每个消息队列设定一个Default Exchange,它的Routing Key是消息队列名称。
RabbitMQ使用实例
Defualt Exchange
1、创建链接,设置MQ连接信息
private static ConnectionFactory connectionFactory = null; private static Connection connection = null;
/*** * 设置MQ连接信息 * @return MQ连接 */ private static ConnectionFactory getConnectionFactory() { // 为防止重复采用一个connectionFactory if (connectionFactory != null){ return connectionFactory; } connectionFactory = new ConnectionFactory(); // 配置连接信息 connectionFactory.setHost("127.0.0.1"); connectionFactory.setPort(5672); connectionFactory.setVirtualHost("/"); connectionFactory.setUsername("admin"); connectionFactory.setPassword("admin"); // 网络异常自动连接恢复 connectionFactory.setAutomaticRecoveryEnabled(true); // 每10秒尝试重试连接一次 connectionFactory.setNetworkRecoveryInterval(10000); Map<String, Object> connectionFactoryPropertiesMap = new HashMap<>(); // 设置MQ属性信息 connectionFactoryPropertiesMap.put("user", "J"); connectionFactoryPropertiesMap.put("description", "My test MQ"); connectionFactoryPropertiesMap.put("emailAddress", "abc@hotmail.com"); connectionFactory.setClientProperties(connectionFactoryPropertiesMap); return connectionFactory; }
2、获取channel通道
/***
* 获取生产者Channel
* @param connectionDescription 生产者名称
* @return 消息通道
*/
public static Channel getChannelInstance(String connectionDescription){
try {
ConnectionFactory connectionFactory = getConnectionFactory();
// 创建一个连接,这里采用1个connection
if (connection == null) {
connection = connectionFactory.newConnection(connectionDescription);
}
// 创建一个channel通道
return connection.createChannel();
} catch (Exception e) {
throw new RuntimeException("获取Channel连接失败");
}
}
3、设置默认的Default Exchange和创建消息队列
/**
* 设置默认的生产Default Exchange
* @param connectionDescription 生产者连接名称
* @param queueName 消息队列名称
* @return channel连接
*/
private static Channel getDefaultExchangeChannel(String connectionDescription, String queueName){
Channel channel = getChannelInstance(connectionDescription);
try {
// 参数,
// 1.队列名称
// 2.是否持久化
// 3.是否只适用于当前TCP连接
// 4.队列不使用时是否自动删除
// 5.定义了队列的一些参数信息,主要用于Headers Exchange进行消息匹配
channel.queueDeclare(queueName,true, false, false, null);
} catch (Exception e) {
System.out.println(e);
}
return channel;
}
4、封装发送消息接口
/**
* 向Default Exchange queue发送消息
* @param connectionDesc 生产者名称
* @param queueName 消息队列名称
* @param message 消息内容
*/
private static void pushMessageToDefaultExchange(String connectionDesc,String queueName, String message){
try {
Channel channel = getDefaultExchangeChannel(connectionDesc, queueName);
// 参数
// 1.Exchange名称,如果没有指定,则使用Default Exchange
// 2.routingKey是消息的路由Key,是用于Exchange将消息路由到指定的消息队列时使用
// 3.props是消息包含的属性信息。RabbitMQ的消息属性和消息体是分开的
// 4.body是RabbitMQ消息体
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("生产者生产消息给队列 "+ queueName + ",内容为:" + message);
} catch (Exception e) {
System.out.println("消息发送到" + queueName + "失败:" + message);
}
}
5、消费者
/**
* 消费默认DefaultExchange的指定队列信息
* @param consumerName 消费者连接名称
* @param queueName 消息队列名称
*/
public static void consumeDefaultExchangeMessage(String consumerName,String queueName){
Channel channel = MQProductsUtils.getChannelInstance(consumerName);
try {
Consumer consumer = new DefaultConsumer(channel){
/**
* 重写消费处理过程
* @param consumerTag 接收到消息时的消费者Tag(随机生成),可以在basicConsume中指定
* @param envelope 消息的属性,包含
* 1.deliveryTag 消息发送编号,
* 2.redeliver 重传标志,确认在收到对消息的失败确认后,是否需要重发这条消息,这里的值为false,不需要重发。
* 3.exchange exchange名称,默认为""
* 4.消息发送的路由Key,这里是发送消息时设置的queueName
* @param properties basicPublish中的props参数
* @param body 消息内容
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者消费消息:" + new String(body));
//System.out.println(consumerTag);
//System.out.println(envelope.toString());
//System.out.println(properties.toString());
// 需要手动确认消息时,通过Channel.basicAck方法,发送确认消息给消息队列
// 参数 1.消息的发送编号 2.确认方式,true为确认所有编号小于等于参数1的消息,false只确认当前消息
// this.getChannel().basicAck(envelope.getDeliveryTag(), false);
}
};
// 这里与消息队列queueName进行绑定,否则无法从队列中消费信息
// 参数 1.绑定的队列名称 2. 自动确认标志,true时自动发送确认消息ACK给队列 3.consume对象,执行处理消息逻辑
channel.basicConsume(queueName, true, consumer);
} catch (Exception e) {
System.out.println(consumerName + "消费" + queueName + "队列消息失败");
}
}
6、测试接口
public static void main(String[] args) {
// 使用默认Exchange,发送一条消息到队列当中
pushMessageToDefaultExchange("测试生产者" ,"FirstQueue", "I am frist queue's message ");
pushMessageToDefaultExchange("测试生产者2" ,"SecondQueue", "I am second queue's message);
consumeDefaultExchangeMessage("测试消费者", "FirstQueue");
consumeDefaultExchangeMessage("测试消费者2", "SecondQueue");
}
Direct Exchange
1、添加 Direct Exchange设置方法
/**
* 设置默认的生产Direct Exchange
* @param connectionDescription 生产者连接名称
* @param queueName 消息队列名称
* @return channel连接
*/
private static Channel getDirectExchangeChannel(String connectionDescription, String queueName){
Channel channel = getChannelInstance(connectionDescription);
try {
// 设置exchange
// 参数1、Exchange名称,2、Exchange类型N
channel.exchangeDeclare("directExchange", "direct");
channel.queueDeclare(queueName, true, false, false, null);
// 绑定exchange
// 参数1、消息队列名称,2、Exchange名称 3、路由匹配规则RoutingKey
channel.queueBind(queueName, "directExchange", "directRoutingKey");
} catch (Exception e) {
System.out.println(e);
}
return channel;
}
2、添加发送消息方法
/**
* 向DirectExchange queue发送消息
* @param connectionDesc 生产者名称
* @param queueName 消息队列名称
* @param message 消息内容
*/
private static void pushMessageToDirectExchange(String connectionDesc,String queueName, String message){
try {
Channel channel = getDirectExchangeChannel(connectionDesc, queueName);
// 参数
// 1.Exchange名称,如果没有指定,则使用Default Exchange
// 2.routingKey是消息的路由Key,是用于Exchange将消息路由到指定的消息队列时使用
// 3.props是消息包含的属性信息。RabbitMQ的消息属性和消息体是分开的
// 4.body是RabbitMQ消息体
channel.basicPublish("directExchange", "directRoutingKey", null, message.getBytes());
System.out.println("生产者生产消息给队列 "+ queueName + ",内容为:" + message);
} catch (Exception e) {
System.out.println("消息发送到" + queueName + "失败:" + message);
}
}
3、消费者
与Default一致,不赘述
其他两种方式这里不再赘述,与Direct基本类似,HeaderExchange了解即可。