一,关于Mq的概念与使用场景
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)
生产者 产生数据发送消息的程序是生产者,如下图的 下单购买商品就是生产者
交换机 交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
队列 队列是 RabbitMQ 内部使用的一种数据结构,本质是大的消息缓冲区,生产者将消息发送到队列中,消费者就可以从这里面拿然后去消费
消费者 顾名思义就是消费消息的一个方法。如下图。同一个方法既可以是生产者也可以是消费者
使用场景,优点1.异步处理数据和解耦,可以增加容错性和减少耗时(简单理解一下)
2.流量削峰,保证服务器正常运行,不要出现服务器瘫痪和宕机
缺点:
系统的可用性降低
系统引入的外部依赖越多,系统越容易挂掉,本来只是A系统调用BCD三个系统接口就好,ABCD四个系统不报错整个系统会正常运行。引入了MQ之后,虽然ABCD系统没出错,但MQ挂了以后,整个系统也会崩溃。
系统的复杂性提高
引入了MQ之后,需要考虑的问题也变得多了,如何保证消息没有重复消费?如何保证消息不丢失?怎么保证消息传递的顺序?
一致性问题
A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产生数据不一致的问题。
二。关于MQ的核心部分(不同的业务选择不同的模式使用,主要是根据公司业务来决定)
Hello Wold 简单模式
Work queues工作 队列模式
Publish/Subscribe发布订阅模式
Routing 路由模式
Topics 主题模式
Publisher Confirms 发布确认模式
三,开始MQ的demo项目,首先创建两个项目或者两个模块都可以,我这里是创建的两个项目springboot+mysql+MQ,项目A表示生产者,项目B表示消费者。(网上关于安装MQ多的很,自己去安装)
1.Hello Wold 简单模式例子
在pom.xml中配置jar包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
</dependencies>
在propertest配置数据库的连接,以及MQ的连接配置(数据库的连接后面有用)
新建一个类,需要配置队列的name,我这里是创建的一个order的队列名字,你们可以自己命名
开始给队列发送消息,我这里是给order这个队列名字发送
到这里之后,就可以在测试内中进行发送消息,只要没报错,就说明没有问题了,我们可以在MQ中查看消息
查看order队列的消息是否存在
我们开始对B项目开始处理(接收者),先引入pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
</dependencies>
配置properties,注意端口号冲突
编写接收类
开始测试接收,刚刚我们发送者发送了一条消息,启动成功可以直接获取
MQ的消息也被接收了(消费)
2.多对多的Work queues工作 队列模式的案例
我们直接复制粘贴一个接收者(每次写完记得重启一下)
然后我们在A项目开始生产信息
开始测试,看是否接收到了10条消息
3.topic exchange(主题模式)
在企业开发中用的比较多,因为这个模式是最灵活的一种方式,可以根据routing_key自由的绑定不同的队列
开始创建主题模式的配置类
开始消息发送TopicService
测试
topic模式可以匹配通配符。*代表匹配一个元素,#代表匹配一个或多个元素。
然后我们就可以去队列中查看到order队列跟order_key队列都有一条消息未被消费
启动两个消费者,他们都能收到消息
4..Fanout Exchange(订阅模式)给Fanout交换机发送消息,绑定了这个交换机的所有队列都收到这个消息
配置队列绑定在交换机中
创建三个消费者 @RabbitListener(queues = "A"),@RabbitListener(queues = "B"),@RabbitListener(queues = "C")
队列的name
开始产生消息(生产者)
测试,成功消费
四.关于MQ的消息丢失,重复,积压的解决方案
1)、消息的丢失
场景1.消息发送出去,由于网络问题没有抵达服务器
解决方案1. 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
2.做好日志记录,每个消息状态是否都被服务器收到都应该记录
3. 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
场景2.消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
解决方案 publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
场景3.自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
解决方案 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
2)、消息的重复
场景1.消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
场景2.消息消费失败,由于重试机制,自动又将消息发送出去(这种是允许的)
场景3.成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
解决方案1消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
解决方案2. 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
解决方案3.rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
3)、消息的积压
场景1.消费者宕机积压
场景2.消费者消费能力不足积压
场景3.发送者发送流量太大
解决方案1.上线更多的消费者,进行正常消费
解决方案2.上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
五.关于解决MQ的消息丢失,重复,积压的简单demo
MQ消息丢失场景1.消息发送出去,由于网络问题没有抵达服务器的处理方式
场景2.消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。设置rabbit的回调机制,但性能下降了一部分
@Configuration
public class MqConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter integrationEventMessageConverter() {
//使用json序列化机制,进行消息转换
return new Jackson2JsonMessageConverter();
}
/**
* 定制rabbitTemplate
* 1.服务器收到消息就回调
* 1.spring.rabbitmq.publisher-confirms=true
* 2.设置确定回调ConfirmCallback
* 2.消息正确抵达队列进行回调
* 1.spring.rabbitmq.publisher-return=true
* spring.rabbitmq.temlate.mandatory=true
* 2.设置确认回调ReturnCallback
*/
@PostConstruct //rabbitTemplate的回调对象
public void initRabbitTemplate(){
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 只要消息抵达Broker就ack=true
* @param correlationData 消息的唯一id
* @param ack 消息是否成功收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
//服务器收到了
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*只要消息没有投递给指定的队列,就触发这个失败回调
* @param message 投递失败的消息详情
* @param i 回复的状态码
* @param s 回复的文本内容
* @param exchange 这个消息是给那个交换机
* @param key 这个消息是用那个路由键
*/
@Override
public void returnedMessage(Message message, int i, String s, String exchange, String key) {
//服务器收到了,但这里报错误了,就要修改数据库当前这条消息的错误状态->错误。
}
});
}
}
场景3.自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机的处理方式,手动ack
MQ消息重复消费场景1.消息消费成功,事务已经提交,该手动ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
MQ消息重复消费场景2.消息消费失败,由于重试机制,自动又将消息发送出去(这种是允许的)
MQ消息重复消费场景3.成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送,其实跟场景1差不多。
MQ消息积压的场景1.消费者宕机积压
MQ消息积压的场景2.消费者消费能力不足积压。
上面这两种就是多加消费者的机器,进行正常消费
MQ消息积压的场景3.发送者发送流量太大
把消息从mq中存放到数据库中的一张表中,然后通过定时任务慢慢的处理完就行了,这样就不影响mq的正常使用