假设一个电商下单场景:
没有使用消息队列
问题一:
用户下单到收到订单成功的时间>=50ms,而且如果过程中某一个模块发生了异常,会导致用户一直等待下单成功的消息,直到bug修复;而实际上用户只需要等待订单模块下单成功即可。
问题二:
如果我要在新增模块,这个模块需要从订单模块获取订单Id然后去处理,就需要在订单模块进行接口调用;每添加一个模块,订单模块都要作出修改,如果哪天这个模块废弃了,又要作出删除;对订单模块的业务负责人来说不仅要维护自己的模块,还要关心别的模块,这显然不负责开发的单一职责。
使用消息队列
解决问题一
只要在下单成功,用户就可以接收到通知20ms,将消息发送给消息队列,其他模块需要处理的时候直接从消息队列中获取数据,实现了异步。
解决问题二
不管是再添加模块还是删除模块,都与订单模块无关,实现了解耦。
项目中的使用场景
在秒杀项目中,极有可能出现供不应求的情况,这时如果还是使用订单模块与数据库更改直接调用,不仅对服务器压力很大,因为是同步的,而且如果用户下单了等了很长时间然后库存没了,这对用户来说体验太差。而如果使用消息队列,当用户请求发出之后,可以在订单模块设置初始库存值,如果库存值大于0,则将请求加入到消息队列,然后库存值-1;如果库存值已经为0,则直接给用户返回下单失败。而下单成功的消息也不用同步的进行处理,这对服务器来说要求也大大降低了。
http://www.rabbitmq.com/download.html
下载依赖Earlang
http://www.erlang.org/downloads
先安装Earlang,再安装RabbitMQ
安装后执行
D:\RabbitMQ\rabbitmq_server-3.7.14\sbin>rabbitmq-plugins enable rabbitmq_management
解决方法:
将 C:\Users\Administrator.erlang.cookie 同步至C:\Windows\System32\config\systemprofile.erlang.cookie
同时删除:C:\Users\Administrator\AppData\Roaming\RabbitMQ目录
输入命令:rabbitmq-plugins.bat enable rabbitmq_management ,出现下面信息表示插件安装成功:
登录
http://localhost:15672
输入guest,guest
AMQP(Advanced Message Queuing Protocol)
高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息发送者无需知道消息使用者的存在。AMQP的主要特征是面向消息、队列、路由、可靠性、安全。AMQP其实和Http协议一样,都是一种协议,只不过Http是针对网络传输的,而AMQP是基于消息队列的。
JMS(Java MessageService)
有Sun公司在其提出的消息标准,旨在为Java应用提供统一的消息操作,包括create、send、receive。JMS和JDBC担任的角色差不多,都是根据相应的接口可以和实现了JMS的服务进行通信,进行相关的操作。
区别
RabbitMQ是一个开源的AMQP的实现,服务端用Erlang语言编写(面向并发的语言)
概述
消息队列是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息来通信,而无需专用链接来连接它们。消息传递是指程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信。
使用场景
在项目中,将一些无需及时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方法大大节省了服务器的请求时间,从而提高了系统的吞吐量。
基本概念
图片转自:https://blog.csdn.net/lizc_lizc/article/details/80722090
Broker
接收和分发消息的应用,我们所说的消息系统就是Message Broker。
Virtual host
出于多用户的安全因素涉及的,把AMQP的基本组件划分到一个虚拟机的分组中,类似于网络中的namespace概念,当多个不同的用户使用同一个RabbitMQ server提供服务时,可以划分出多个vhost,每个用户在自己vhost创建exchange/queue等。
Connection
是RabbitMQ的socket连接,封装了socket协议相关部分逻辑;
Channel
是我们与RabbitMQ打交道的最重要的一个接口,大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue和Exchange等。如果每次访问RabbitMQ都建立一个Connection,在消息量打的时候建立TCP Connection的开销将是巨大的,效率也低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的Channel进行通信。AMQP method包含了Channel id帮助客户端和message broker识别Channel,所以Channel之间是隔离的,极大减少了操作系统建立TCP connection的开销。
Queue
用于存储消息;
生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费;注意,当多个消费者同时订阅同一个Queue时,Queue中的消息会被平均分配给多个消费者进行处理。
Exchange
实际情况中,生产者将消息发送到Exchange,由Exchange将消息按照指定的路由策略路由到一个或多个Queue中。
Binding
RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。
routing key
生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,一般需要与Exchange Type以及binding key联合使用才能生效。
在Exchange Type与binding key固定的时候,一般都是固定配置的,通过指定routing key来决定消息流向哪里,长度限制为255 bytes;
Binding key
消费者将消息发送给Exchange是,一般会指定一个routing key,当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。
生成连接的类
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("192.168.229.129");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/leyou");
factory.setUsername("/leyou");
factory.setPassword("leyou");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
消费者获取消息
/**
* 消费者
*/
public class Recv {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
RabbitMQ是一个消息代理:它接收和转发消息,可以把它当成一个邮局,当你把邮件放在邮箱时,你可以确定邮差先生最终会把邮件发送给你的收件人。RabbitMQ主要是接收、存储和转发数据消息的而进行数据块。
生产者发送消息
/**
* 生产者
*/
public class Send {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道,使用通道才能完成消息相关的操作
Channel channel = connection.createChannel();
// 声明(创建)队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息内容
String message = "Hello World!";
// 向指定的队列中发送消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//关闭通道和连接
channel.close();
connection.close();
}
}
基本消息模型中是从一个命名队列中发送并接收消息,在这里,将创建一个工作队列,在多个工作者之间分配耗时任务。
工作队列,又称为任务队列,主要思想就是避免执行资源密集型任务时,一个消费者忙不过来,所以可以同时让几个消费者监听队列,等于是将任务分派给几个消费者。
生产者
public class Send {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 循环发布任务
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "task .. " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(i * 2);
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
消费者1
消费者2
上述案例出现的问题:
现在的状态属于把任务平均分配,正确的做法应该是消费越快的人,消费越多。
我们可以使用basicQos方法将prefetchCount设置为1,这告诉RabbitMQ一次不要向消费者发送多于一条消息。直至它处理并确认前一个消息,相反,它会将其分派给不是仍然忙碌的下一个消费者。
常用的有四种交换机策略fanout(分列法)、direct(直接法)、topic(主题匹配)和headers;
fanout
它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
//创建fanout交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding fanoutBinding1(){
return BindingBuilder.bind(topicQueue1()).to(fanoutExchange());
}
@Bean
public Binding fanoutBinding2(){
return BindingBuilder.bind(topicQueue2()).to(fanoutExchange());
}
public void sendFanout(Object message){
String msg = RedisService.beanToString(message);
logger.info(msg);
amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE,"",msg);
}
可以看到queue1和queue2都接收到了消息
direct
会把消息路由到那些binding key与routing key完全匹配的Queue中;
topic
topic在匹配规则上进行了扩展,与direct类型的Exchange相似,但是进行扩展,类似于正则表达式匹配的方式,如果哪个Queue上的binding key与routing key匹配成功就分配消息哪个Queue。如果没有匹配到任何一个则消息会被丢弃;
public static final String TOPIC_QUEUE1="topic_queue1";
public static final String TOPIC_QUEUE2="topic_queue2";
public static final String TOPIC_EXCHANGE="topic_exchange";
/**
* Topic交互模式
*/
@Bean
public Queue topicQueue1(){
return new Queue(TOPIC_QUEUE1,true);
}
@Bean
public Queue topicQueue2(){
return new Queue(TOPIC_QUEUE2,true);
}
//创建一个topic交换机
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(TOPIC_EXCHANGE);
}
//给创建的topic类型交换机 绑定queue队列并指定binding key需要匹配的rounting key的规则
@Bean
public Binding topicBinding1(){
return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.key1");
}
@Bean
public Binding topicBinding2(){
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.#");
}
消息发送端
public void sendTopic(Object topicMessage){
String msg = RedisService.beanToString(topicMessage);
logger.info(msg);
//指定交互机名称,指定routing key,放入要发送的消息
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE,"topic.key1",msg+"1");
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE,"topic.key2",msg+"2");
}
消息处理端
@RabbitListener(queues = MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message){
logger.info("topic queue1 message:"+message);
}
@RabbitListener(queues = MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message){
logger.info("topic queue2 message:"+message);
}
可以看到队列2接收到了两个消息,因为之前指定它的binding key匹配为topic.#所以可以匹配两个,而队列1的binding key只能匹配topic.key1这一个。
headers
不依赖于binding key和routing key的匹配规则来路由消息。而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对,当消息发送到Exchange 时,RabbitMQ会取到该消息的headers,对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对。
@Bean
public Binding headersBinding(){
//创建map并指定匹配键值对
HashMap<String, Object> map = new HashMap<>();
map.put("header1","value1");
map.put("header2","value2");
//表示所有的键值对都匹配上这个消息才能入队
return BindingBuilder.bind(headersQueue()).to(headersExchange()).whereAll(map).match();
}
public void sendheaders(Object message){
String msg = RedisService.beanToString(message);
logger.info("send headers message:"+msg);
MessageProperties properties = new MessageProperties();
properties.setHeader("header1","value1");
properties.setHeader("header2","value2");
Message obj = new Message(msg.getBytes(), properties);
amqpTemplate.convertAndSend(MQConfig.HEADWES_EXCHANGE,"",obj);
}
@RabbitListener(queues = MQConfig.HEADERS_QUEUE)
public void receiveHeaders(byte[] message){
logger.info("headers queue message:"+new String(message));
}
MQ本身是基于异步消息处理,但实际情况中,我们很可能需要一些同步处理,需要同步等待服务端将消息处理完后再进行下一步处理。
RabbitMQ中实现RPC的机制是
客户端发送请求时,在消息的属性中设置两个值replyTo(一个Queue名称,用于高速苏武器处理完成后将通知我的消息发送到这个Queue)和correlationld(此请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解那条请求被成功执行了或执行失败了)
添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
配置
#rabbitmq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#消费者的数量
spring.rabbitmq.listener.simple.concurrency= 10
spring.rabbitmq.listener.simple.max-concurrency= 10
#从队列中取,每次取几个
spring.rabbitmq.listener.simple.prefetch= 1
#消费者自动启动
spring.rabbitmq.listener.simple.auto-startup=true
# 消费者消费失败后重新加到队列
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#\u542F\u7528\u53D1\u9001\u91CD\u8BD5
spring.rabbitmq.template.retry.enabled=true
#spring.rabbitmq.template.retry.initial-interval=1000
spring.rabbitmq.template.retry.max-attempts=3
#spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
创建消息接收者
@Service
public class MQReceiver {
private static Logger logger=LoggerFactory.getLogger(MQReceiver.class);
@Autowired
AmqpTemplate amqpTemplate;
@RabbitListener(queues = MQConfig.QUEUE)
public void receive(String message){
logger.info("receiver"+message);
}
}
创建消息发送者
@Service
public class MQSender {
@Autowired
AmqpTemplate amqpTemplate;
private static Logger logger=LoggerFactory.getLogger(MQReceiver.class);
public void sendMiaoshaMessage(Object message){
String msg = RedisService.beanToString(message);
logger.info("Send"+msg);
amqpTemplate.convertAndSend(MQConfig.QUEUE,msg);
}
}
MQ的配置类
@Configuration
public class MQConfig {
public static final String QUEUE="queue";
@Bean
public Queue queue(){
//第一个参数指明队列的名字,第二个参数指明是否支持持久化
return new Queue(QUEUE,true);
}
}
消息一旦被消费者接收,队列中的消息就会被删除
消费者收到Queue中的消息,但是没有处理完就宕机的情况,这种情况可能会造成消息丢失。为了避免这种情况的发生,可以要求消费者在消费完一个消息后发送一个回执给RabbitMQ,RabbitMQ收到回执后,才能将队列中的消息移除;
这需要看消息的重要性:
public class Recv2 {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [x] received : " + msg + "!");
// 手动进行ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,第二个参数false,手动进行ACK
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则它会将该消息重新发送给别的消费者进行处理。注意,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非检测到连接断开。
如果希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue和Message设置为可持久化的。但还是避免不了小概率的丢失事件,如还没来得及持久化的消息,突然断电。这里可以使用事务来管理这种小概率的消息丢失事件。
交换机持久化
队列持久化
消息持久化