MQ(Message Queue): 是一种跨进程的通信机制, 用于上下游传递消息 . 在互联网架构中, MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务 .
JMS(Java Message Server) : java 消息服务应用程序接口, 是一个java平台面向消息中间件的技术规范(API接口规范) .
Amqp(Advanced Message Queuing Protocol) : 高级消息队列协议使得遵从该规范的客户端应用和消息中间件服务器的全功能互操作成为可能. 跨平台,跨语言.
RabbitMq是采用erlang语言编写的,安装Rabbitmq之前,必须安装erlang,
常见安装错误: 1 安装Erlang时openssl版本太低出错; 2 erlang和rabbitmq版本不匹配 ;
安装教程
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>3.4.1version>
dependency>
dependencies>
1 创建连接
package com.wunlie.rabbitmq.demo1;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.wunlie.rabbitmq.config.RabbitConfig;
/**
* @Description: rabbit 连接工具
* @author: wangjie
* @createAt: 2019-09-09-15:07
*/
public class ConnectionUtil {
public static Connection getRabbitConnection() throws Exception{
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost(RabbitConfig.HOST);
//端口
factory.setPort(RabbitConfig.PORT);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost(RabbitConfig.VIRTUAL_HOST);
factory.setUsername(RabbitConfig.NAME);
factory.setPassword(RabbitConfig.PASS);
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
2 发送消息
package com.wunlie.rabbitmq.demo1;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wunlie.rabbitmq.config.RabbitConfig;
/**
* @Description: 发送消息
* @author: wangjie
* @createAt: 2019-09-09-15:17
*/
public class Send {
public static void main( String[] args ) throws Exception {
/**
* 1 创建连接
*/
Connection connection = ConnectionUtil.getRabbitConnection();
/**
* 2 从连接中创建通道
*/
Channel channel = connection.createChannel();
/**
* 3 创建队列
* String queue 队列名称,
* boolean durable 持久的,
* boolean exclusive 专有的,独有的,
* boolean autoDelete 是否自动删除,
* Map arguments 队列绑定的参数 如:超时时间,死信交换机
*/
channel.queueDeclare(RabbitConfig.QUEUE_NAME,false,false,false,null);
String message = "hello word!";
/**
* 发送消息
* String exchange, 交换机
* String routingKey, 路由
* boolean mandatory, [可选] 为true时如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。
* 为false时出现上述情形broker会直接将消息扔掉
* boolean immediate, [可选] 为true时如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。
* 当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
* BasicProperties props, 属性参数 14个属性
* byte[] body 消息体
*/
channel.basicPublish("",RabbitConfig.QUEUE_NAME,null,message.getBytes());
channel.close();
connection.close();
}
}
3 接受消息
package com.wunlie.rabbitmq.demo1;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.wunlie.rabbitmq.config.RabbitConfig;
/**
* @Description: 接收者
* @author: wangjie
* @createAt: 2019-09-09-15:41
*/
public class Recv {
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getRabbitConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(RabbitConfig.QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(RabbitConfig.QUEUE_NAME, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
}
注意: 消息体属性参数
public static class BasicProperties extends AMQBasicProperties {
/**
* 消息类型如(text/plain)
*/
private String contentType;
/**
* 消息内容编码
*/
private String contentEncoding;
/**
*
*/
private Map headers;
/**
* 消息持久化
* 1 不持久化 2 持久化
*/
private Integer deliveryMode;
/**
* 优先级
* 0到 9
*/
private Integer priority;
/**
* 相关标识 - 与这个相关的消息,用于将RPC响应与请求相关联, 比如 请求这里消息的请求。 建议应用程序使用这里属性,而不是将这里信息放入消息负载。
* 值:任何值
*/
private String correlationId;
/**
* 通常用于命名回调队列,(用于指定回复的队列的名称)
*/
private String replyTo;
/**
* 过期时间
*/
private String expiration;
/**
* 消息id
*/
private String messageId;
/**
* 发送消息时的时间戳
*/
private Date timestamp;
/**
* 类型
*/
private String type;
/**
* 用户id
*/
private String userId;
/**
* 应用程序id
*/
private String appId;
/**
*
*/
private String clusterId;
}
The core idea in the messaging model in RabbitMQ is that the producer never sends any messages directly to a queue. Actually, quite often the producer doesn’t even know if a message will be delivered to any queue at all.
Instead, the producer can only send messages to an exchange. An exchange is a very simple thing. On one side it receives messages from producers and the other side it pushes them to queues. The exchange must know exactly what to do with a message it receives. Should it be appended to a particular queue? Should it be appended to many queues? Or should it get discarded. The rules for that are defined by the exchange type.
rabbitmq消息传递模型的核心思想是,生产者从不将任何消息直接发送到队列。实际上,生产者常常根本不知道消息是否会被传递到任何队列。
相反,生产者只能向交换发送消息。交换是一件很简单的事。一方面它接收来自生产者的消息,另一方面它将接收到的消息推送到队列中。交换必须确切地知道如何处理它接收到的消息。是否应将其附加到特定队列?它应该附加到许多队列中吗?或者应该被丢弃。其规则由交换类型定义。
交换机的类型有:direct
, topic
, fanout
, headers
. 通常我们只会用到前面三种
交换机和队列创建之后, 交换必须确切地知道如何处理它接收到的消息
,所以我们必须告诉交换机将消息推送给哪个或哪些队列, 交换机与队列之间的这个关系称之为绑定(Binding), 通过这种绑定关系, 交换机处理它接受的消息.
生产者发送消息时,会指定交换器(exchange)和路由键(Routing key), 交换机会根据绑定键(Binding key),将消息推送到对应的队列(Queue)上.
广播类型(fanout)的交换机, 没有绑定键(Binding key), 所以生产者在发送消息时不需要指定路由键(Routing key)
直连类型(direct)的交换机, 只有当 Binding key == Routing key 时, 交换器才会将消息推送给对应的队列上, 而且直连交换机支持以一个绑定键(Binding key)与多个队列绑定.
主题交换机(topic)的交换机, 绑定键(Binding key)可以含有通配符, 在Routing Key满足通配符的条件下,交换机将消息推送给对应的队列. * 可以是一个或多个单词, # 是一个单词.
在3种情况下, 消息会变成死信:
死信交换机(DLX): dead-letter-exchange, 它也只是一个正常的交换机, 通过属性x-dead-letter-exchange
绑定到队列上,
当队列中的消息成为死信时, RabbitMQ就会自动将这个消息重新推送到设置的死信交换机(DLX)上, 进而贝推送到相应的队列上, 可以监听这个队列,对死信进行相应的处理.
public class Provider {
public static void main( String[] args ) throws Exception {
Connection connection = ConnectionUtil.getRabbitConnection();
Channel channel = connection.createChannel();
Map arguments = new HashMap(16);
// 为队列设置队列交换器
arguments.put("x-dead-letter-exchange", RabbitConfig.dlxExchangeName);
// 设置队列中的消息 10s 钟后过期
arguments.put("x-message-ttl", 30000);
//arguments.put("x-dead-letter-routing-key", "为 dlx exchange 指定路由键,如果没有特殊指定则使用原队列的路由键");
//交换机
channel.exchangeDeclare(RabbitConfig.orderExchangeName, "topic", true, false, null);
//队列
channel.queueDeclare(RabbitConfig.orderQueueName, true, false, false, arguments);
//绑定
channel.queueBind(RabbitConfig.orderQueueName, RabbitConfig.orderExchangeName, RabbitConfig.orderRoutingKey);
// 创建死信交换器和队列
channel.exchangeDeclare(RabbitConfig.dlxExchangeName, "topic", true, false, null);
channel.queueDeclare(RabbitConfig.dlxQueueName, true, false, false, null);
channel.queueBind(RabbitConfig.dlxQueueName, RabbitConfig.dlxExchangeName, RabbitConfig.orderRoutingKey);
for (int i = 0; i < 10; i++) {
String message = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 创建订单." + i;
//设置消息的过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.contentEncoding("UTF-8")
.expiration("10000")
.build();
channel.basicPublish(RabbitConfig.orderExchangeName, "order.save",
properties,
message.getBytes("UTF-8"));
Thread.sleep(5000);
}
System.err.println("消息发送完成......");
}
}
消息的延迟发送,有两种实现方式, 1 利用TTL; 2 使用插件rabbitmq-delayed-message-exchange
//1 采用TTL
@Configuration
public class TtlExchangeAndQueueConfig {
// 创建一个立即消费队列
@Bean
public Queue immediateQueue() {
// 第一个参数是创建的queue的名字,第二个参数是是否支持持久化
return new Queue(Constants.IMMEDIATE_QUEUE, true);
}
// 创建一个延时队列
@Bean
public Queue delayQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", Constants.IMMEDIATE_EXCHANGE);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", Constants.IMMEDIATE_ROUTING_KEY);
return new Queue(Constants.DELAY_QUEUE, true, false, false, params);
}
@Bean
public DirectExchange immediateExchange() {
// 一共有三种构造方法,可以只传exchange的名字,
// 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
// 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
return new DirectExchange(Constants.IMMEDIATE_EXCHANGE, true, false);
}
@Bean
public DirectExchange deadLetterExchange() {
// 一共有三种构造方法,可以只传exchange的名字, 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
//第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
return new DirectExchange(Constants.DEAD_LETTER_EXCHANGE, true, false);
}
@Bean
//把立即消费的队列和立即消费的exchange绑定在一起
public Binding immediateBinding() {
return BindingBuilder.bind(immediateQueue()).to(immediateExchange()).with(Constants.IMMEDIATE_ROUTING_KEY);
}
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue()).to(deadLetterExchange()).with(Constants.DELAY_ROUTING_KEY);
}
}
//2 使用插件`rabbitmq-delayed-message-exchange
@Configuration
public class ExchangeAndQueueConfig {
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
/**
* name: 交换机名称
* type: 类型
* durable: 持久化
* autoDelete: 自动删除
* args: 参数
*/
return new CustomExchange("test_exchange", "x-delayed-message",true, false,args);
}
@Bean
public Queue queue() {
Queue queue = new Queue("test_queue_1", true);
return queue;
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(delayExchange()).with("test_queue_1").noargs();
}
}
推送消息:
@Component
@Slf4j
public class RabbitmqProviderServer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessageWithTtl( String msg, int delayTime){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("sendMessageWithTtl 消息发送时间:"+sdf.format(new Date()));
rabbitTemplate.convertAndSend(Constants.DEAD_LETTER_EXCHANGE, Constants.DELAY_ROUTING_KEY, msg, message -> {
message.getMessageProperties().setExpiration(delayTime + "");
return message;
});
}
/**
*
* @param msg
* @param delayTime 毫秒
*/
public void sendMessageWithPlugins( String msg, int delayTime){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("sendMessageWithPlugins 消息发送时间:"+sdf.format(new Date()));
rabbitTemplate.convertAndSend("test_exchange", "test_queue_1", msg, new MessagePostProcessor() {
@Override
public Message postProcessMessage( Message message) throws AmqpException {
message.getMessageProperties().setHeader("x-delay",delayTime);
return message;
}
});
}
}
c
//测试
@Test
public void send() throws Exception{
for (int i = 3; i > 0; i--) {
int delayTime = 5000 * i;
String msg = "ttl 发送消息,当前时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
messageService.sendMessageWithTtl(msg,delayTime);
Thread.sleep(1000);
}
}
@Test
public void send2()throws Exception {
for (int i = 3; i > 0; i--) {
int delayTime = 5000 * i;
String msg = "plugin 发送消息,当前时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
messageService.sendMessageWithPlugins(msg,delayTime);
Thread.sleep(3000);
}
}
要求: 模拟发送三条消息 , 第一条在发送后15秒后消费,第二条在发送后10秒后消费; 第三条在发送后5秒消费.
测试结果:
消息消费时间:[2019-09-18 16:04:24] 消息内容:[ttl 发送消息,当前时间:2019-09-18 16:04:09]
消息消费时间:[2019-09-18 16:04:24] 消息内容:[ttl 发送消息,当前时间:2019-09-18 16:04:10]
消息消费时间:[2019-09-18 16:04:24] 消息内容:[ttl 发送消息,当前时间:2019-09-18 16:04:11]
消息消费时间:[2019-09-18 16:04:43] 消息内容:[plugin 发送消息,当前时间:2019-09-18 16:04:38]
消息消费时间:[2019-09-18 16:04:45] 消息内容:[plugin 发送消息,当前时间:2019-09-18 16:04:35]
消息消费时间:[2019-09-18 16:04:47] 消息内容:[plugin 发送消息,当前时间:2019-09-18 16:04:32]
测试结果说明, 采用TTL 发送延迟消息,并不能达到我们自有自定义延迟消费时间的要求, 而采用插件形式可以实现.
从结果可以看出,采用TTL形式,在队列中的第一条消息没有消费之前,后面的消息不能被消费. 采用插件形式,消息的消费是按照延迟时间到期的先后顺序消费的, 并不一定是"先进先出"消费的.
在具体的功能业务中,并不一定是先发送的消息需要先消费. 比如营销活动被审核通过之后,并不是马上就会上线使用,也可能后审核通过的营销活动先上线,在这种情况下,我们就需要采用插件形式; 还有一种情况,比如订单在创建之后,如果30分钟没有支付,订单的状态就需要改成为"已取消", 这种情况下,消息是"先进先出"的,我们就可以采用TTL的形式.