MQ的概念
什么是MQ
MQ: message queue 消息队列。
MQ的优点
(1)异步提速
(2)降低耦合
(3)削峰填谷
MQ的缺点
(1)复杂度提高
(2)处理结果的前后一致性变得不同步
(3)高可用性变差
常见MQ的产品
ActiveMQ : JMS接口 JAVA
RabbitMQ: AMQP协议 erlang
RocketMQ: JMS JAVA
ZeroMQ: C语言写的
Kafka:大数据那边 海量数据
RabbitMQ的概述
RabbitMQ的结构
Producer:负责发送消息
Connection/Channel:建立生产者和消费者与RabbitMQ的连接
Exchange:交换机,接收并分发消息,不存储消息
Queue:存储消息 (先进先出)
RoutingKey:路由,建立交换机分发消息的规则。建立Exchange和Queue之间的关系
Consumer:消费消息
RabbitMQ的工作模式
简单模式
工作队列模式
消费者会以轮询的方式从队列中获取消息进行消费。
发布订阅模式
Exhcange: fanout
路由模式
Exhcange:direct
通配符模式
Exchange:topic
RabbitMQ的开发
方式一:原生API
//1.创建连接工厂 [五要素:host port username password virtualHost]
//2.获取一个新的连接
//3.获取通道
//4.声明队列
//5.声明交换机
//6.绑定交换机和队列之间的关系
//7.发送消息
//8.关闭连接资源
生产者
//1.创建连接工厂 [五要素:host port username password virtualHost]
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
//2.获取一个新的连接
Connection connection = connectionFactory.newConnection();
//3.获取通道
Channel channel = connection.createChannel();
//4.声明队列 queue declare声明
channel.queueDeclare("queueName",true,false,false,null);
//5.声明交换机 exchange
channel.exchangeDeclare("exchangeName", BuiltinExchangeType.FANOUT);
//exchangeType: fanout 发布订阅 direct 路由 topic 通配符
//6.绑定交换机和队列之间的关系
channel.queueBind("queueName","exchangeName","routingKey");
//7.发送消息
channel.basicPublish("exchangeName","routingKey",null,"body".getBytes());
//8.关闭连接资源
channel.close();
connection.close();
消费者
//1.创建连接工厂 [五要素:host port username
password virtualHost]
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
//2.获取一个新的连接
Connection connection = connectionFactory.newConnection();
//3.获取通道
Channel channel = connection.createChannel();
//4.声明队列 queue declare声明
channel.queueDeclare("queueName",true,false,false,null);
//5.声明交换机 exchange
channel.exchangeDeclare("exchangeName", BuiltinExchangeType.FANOUT);
//exchangeType: fanout 发布订阅 direct 路由 topic 通配符
//6.绑定交换机和队列之间的关系
channel.queueBind("queueName","exchangeName","routingKey");
//7.接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是获取的消息内容
//业务处理
}
};
channel.basicConsume("queueName",true,consumer);
//8.关闭连接资源 消费者是不能关闭连接
//channel.close();
//connection.close();
方式二:与Spring整合
生产者
(1) 导入对应的jar包
(2)添加配置文件
连接RabbitMQ
Queue
Exchange
绑定Queue和Exchange之间的关系
发送消息 RabbitTemplate.convertAndSend()
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--建立和RabbitMQ的连接 -->
<context:property-placeholder location="classpath:properties/rabbitmq.properties"/>
<rabbit:connection-factory
id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
/>
<rabbit:admin connection-factory="connectionFactory"/>
<!--声明队列-->
<rabbit:queue id="queueName" name="queueName" auto-declare="true"/>
<!--声明交换机-->
<rabbit:fanout-exchange id="fanoutExchange" name="fanoutExchange">
<!--绑定Queue和Exchange -->
<rabbit:bindings>
<rabbit:binding queue="queueName"></rabbit:binding>
</rabbit:bindings>
</rabbit:fanout-exchange>
<rabbit:direct-exchange id="directExchange" name="directExchange">
<!--绑定Queue和Exchange -->
<rabbit:bindings>
<rabbit:binding queue="queueName" key="routingKey"></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<rabbit:topic-exchange id="topicExchange" name="topicExchange">
<!--绑定Queue和Exchange
#:零个或多个
*: 一个
-->
<rabbit:bindings>
<rabbit:binding queue="queueName" pattern=""></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!--RabbitTemplate-->
<rabbit:template connection-factory="connectionFactory" id="rabbitTemplate"/>
(3)注入对应Bean对象进行业务操作
@Autowired
private RabbitTemplate rabbitTemplate;
rabbitTemplate.convertAndSend("exchangeName","routingKey",body)
;
消费者
(1) 导入对应的jar包
(2)添加配置文件
连接RabbitMQ
监听队列操作
监听器类
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--建立和RabbitMQ的连接 -->
<context:property-placeholder location="classpath:properties/rabbitmq.properties"/>
<rabbit:connection-factory
id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
/>
<rabbit:admin connection-factory="connectionFactory"/>
<!-- 方式一-->
<bean id="myListener" class="监听器类的类全名"/>
<!--方式二
注解 @Component
包扫描,扫描到对应的注解
-->
<rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
<!--
ref指定的是Spring容器中的bean对象,监听器类
-->
<rabbit:listener ref="myListener" queue-names="queueName"/>
</rabbit:listener-container>
</beans>
(3)编写业务操作
public class Xxxx implements MessageListener{
onMessage(Message message){
//获取消息
//业务操作
}
}
方式三:与SpringBoot整合
生产者
(1)pom.xml添加起步依赖
(2)application.yml添加配置
(3)队列、交换机、绑定队列和交换机之间的关系的声明
(4)发送消息
(1)
spring-boot-starter-amqp
(2)
spring:
rabbitmq:
host: 172.16.98.133 # ip
port: 5672
username: guest
password: guest
virtual-host: /
(3)@Configuration @Bean
@Configuration
public class MyConfigure {
//声明队列
@Bean(name="queueName")
public Queue queue(){
return QueueBuilder.durable("queueName").build();
}
//声明交互机
@Bean(name="exchangeName")
public Exchange exchange(){
//return ExchangeBuilder.fanoutExchange("exchangeName").build();
//return ExchangeBuilder.directExchange("exchangeName").build();
return ExchangeBuilder.topicExchange("exchagenName").build();
}
//绑定关系
@Bean
public Binding binding(@Qualifier("queueName")Queue queue
,@Qualifier("exchangeName") Exchange exchange){
//bind: 队列对象
//to :交换机对象
return BindingBuilder.bind(queue).to(exchange).with("routingKey").noargs();
}
}
(4)发送消息
@Autowired
private RabbitTemplate rabbitTemplate; //思考 RabbitTemplate从哪里来???
rabbitTemplate.convertAndSend("exchangeName","routingKey",body);
消费者
(1)pom.xml添加起步依赖
(2)application.yml添加配置
(3)监听队列获取消息
(1)
spring-boot-starter-amqp
(2)
spring:
rabbitmq:
host: 172.16.98.133 # ip
port: 5672
username: guest
password: guest
virtual-host: /
(3)监听类
@Component
public class MyListener{
@RabblitListener(queues="queueName")
public void method(String message){
//获取消息
//对应业务操作
}
}
RabbitMQ高级部分
消息的可靠性投递
<rabbit:connection-factory
id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-confirms="true"
publisher-returns="true"
/>
RabbitTemplate类调用对应的方法来添加回调函数。
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//ack true 消息已经正确发送到交换机 false 消息未能正确发送到交换机,
//如果ack是false,couse就是未能正确发送到交换机的原因
}
});
//设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
/**
* message:消息对象
* replyCode:错误码
* replyText:错误信息
* exchange:交换机
* routingKey:路由
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了...");
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
}
});
<!--
acknowledge:控制的是消息接收的模式
manual: 手动确认
none: 自动确认 默认
如果是自动确认,消息被消费者取走以后,队列中的消息就会被删除,不管消息有没有正确被消费
如果是手动确认,就需要程序员在做业务处理的时候,根据实际情况来决定是否要将消息从队列中删除掉
监听器类的编写
public class MyListener implmenets MessageListener{
onMessage(Message message){
//要想手动确认消息,需要用到Channel对象,现在这种方式无法获取到channel
}
}
解决方案
@Component
public class AckListener implements ChannelAwareMessageListener{
@Override
public void onMessage(Message message, Channel channel) throws Exception {
System.out.println(new String(message.getBody()));
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try{
Thread.sleep(1000);
//处理业务逻辑
//System.out.println("处理出错误了");
//int i = 10/0;
System.out.println("处理业务逻辑");
channel.basicAck(deliveryTag,true); //手动确认正确接收
}catch (Exception e){
channel.basicNack(deliveryTag,true,true);//手动确认接收异常
}
}
}
-->
<rabbit:listener-container connection-factory="connectionFactory" auto-declare="true"
acknowledge="manual">
<rabbit:listener ref="ackListener" queue-names="queueName"/>
</rabbit:listener-container>
消费端限流
TTL
死信队列
当消息未正常消费后,需要将失败的消息存入到指定的队列中,该队列就是死信队列。
三种情况可以让消息进入死信队列:
(1)消息队列的长度查出,比如设置消息队列能存储100个消息,当队列中超过100个后,进入队列的消息就会被送入死信队列中
(2) 消息队列设置了TTL,当消息超时未被消费的消息进入死信队列
(3) 消费者在消费消息的时候,如果业务处理失败,被消费者通过channel.basicNack();进行拒收的消息也会进入到死信队列。
延迟队列
死信队列中的第二种情况,用来实现延迟队列的。
消息的补偿
消息的补偿就是确保消息100%被消费。
(1)记录生产者发送的消息数
(2)记录消费者消费消息数
(3)获取两个消息数的差值,差值代表的含义就是未被正确消费的消息,将这些消息重新进行消息的发送。
消息的幂等性
消息被消费者消费多次最终获取的结果要保持一致。
判断消息是否被消费过,如果未被消费过,让消费者进行消费,如果已经消费过,不在让消费者进行消费了。
数据库Mysql表:
table
id name … version
1 x … 2
(1) update table set version = version+1 where version =1 … 更新成功,说明消息未被消费过,让消费者进行消费操作。
(2) update table set version = version+1 where version =1 … 更新失败 说明消息已经被消费过了,不在进行消费。
Redis的key-value
key是否存在来判断