目录
1.1消息从生产者到交换机有可能会丢失。这里可以通过confirm机制来解决
1.2交换机到队列也有可能会丢失。这里可以通过return机制来解决
1.33、从队列到消费者也有可能会丢失。这里可以通过手动ACK解决。
因为像我们之前的项目,代码之间的执行都是同步的,一个业务的处理必须等待上一个业务的完成,这样就比较耗费时间,比如我们的用户查询数据的时候,对于用户而言他只需要查寻数据这一个操作,对于我们服务端而言可能还需要做一些处理,像存入缓存、删除缓存,只有做完这些操作我们服务端才会把数据传给用户,但是这些是我们业务的处理,不应该让用户来承担这样的一个时间成本,并且用户等待数据的时间过长,给用户也会带来了很不好的体验感,同时模块之前的耦合性很高,一个模块宕机后,全部模块都不能用了。所以要中间件RabbitMQ
就是rabbitMq有一个生产者模块负责发送消息到队列中,一个消费者模块负责从队列中拿到数据进行消费,模块与模块间分离,通过RabbitMq进行数据通信
它可以不需要等待我们代码的全部执行,就可以用户所需要的数据将消息发送到队列中,然后由队列推给用户
一个生产者,一个队列,一个消费者
一个生产者,一个队列,多个消费者
采用多个消费者是为了加快消息的消费,多个消费者之间采用轮训的方式。
一个生产者,一个交换机 ,多个队列,一个队列上对应一个消费者,消费者只有订阅才能收到消息,交换机通过广播给订阅的队列
路由键模型,跟发布订阅一样,只不过多了个路由键,进行一个条件的判断
主体模型跟路由键类似,只不过路由键多了两个符号 *代表可以接单个字符,# 代表可以接多个字符
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-amqpartifactId>
- dependency>
- spring:
- rabbitmq:
- host: 192.168.107.123 #虚拟主机的ip地址
- port: 5672 #RabbitMq的端口号
- username: guest #匿名用户
- password: guest
- virtual-host: / # 虚拟机主机,队列就是保存在虚拟主机中
-
- // 交换机的类型是路由键
- @Bean
- public DirectExchange directExchange() {
- return new DirectExchange("direct-exchange");
- }
-
- @Bean
- public Queue directQueue1() {
- return new Queue("direct-queue1");
- }
-
- @Bean
- public Queue directQueue2() {
- return new Queue("direct-queue2");
- }
-
- // 绑定
- @Bean
- public Binding directQueue1Bind() {
- // 给direct-queue1绑定了两个队列
- BindingBuilder.DirectExchangeRoutingKeyConfigurer to = BindingBuilder.bind(directQueue1()).to(directExchange());
-
- // 绑定了两个路由键
- to.with("error");
- Binding warn = to.with("info");
- return warn;
- }
-
- @Bean
- public Binding directQueue2Bind() {
- return BindingBuilder.bind(directQueue1()).to(directExchange()).with("info");
- }
- @Test
- void contextLoads() {
- rabbitTemplate.convertAndSend("direct-exchange", "error", "toString");
- System.out.println("生产者消息发送完成");
- }
- @Component
- public class HelloQueueListener {
-
- @RabbitListener(queues = "direct-queue1")
- public void cosnuermMsg(String msg) {
- System.out.println("消费者拿到的数是:" + msg);
- }
- }
ACK机制是消费端的一个消息确认机制
MQServer把消息推送给消费者后,消费者开始消费,消费完成后需要把结果给MQServer应答一下,消费结果有两种情况:失败、成功
消费成功:应答ACK,MQServer手动ACK后就明白这个消息已经被成功的消费了,可以从队列中删除了。
消费失败:应答NACK。MQServer收到NACK后知道了消费者无法消费这个消息,发送给其他的消费者进行消费。如果其他的消费者也是无法消费,此时需要这类消息全部的收集起来入库,通知相关人员来检查。
消费者默认自动应答,不出异常自动应答,出了异常应答NACK,并且把这个消息压入到队列
ready:待分配(消费者)的消息的数量。
unackded:待应答的消息数量。
total:总消息的数量。
当出现异常没有处理时候,那么被认为应答nACK,消息回到队列的待应答状态,关掉消费者,则进入待分配状态
客户端先创建连接对象,有了连接对象才能创建信道进行数据的传输channel断开后把待应答的消息,全部变为待分配。
mqServer是支持持久化的,重启后数据还有,down删除就没有了
1.ylm配置文件
- spring:
- rabbitmq:
- host: 192.168.127.102
- port: 5672
- username: guest
- password: guest
- virtual-host: /
- listener:
- simple:
- acknowledge-mode: manual # 手动ACK
- @RabbitListener(queues = "hello-queue")
- public void cosnuermMsg(String msg, Channel channel, Message message) {
- System.out.println("消费者拿到的数是:" + msg);
-
- // 每个消息的标识
- long deliveryTag = message.getMessageProperties().getDeliveryTag();
- try {
- // 开始消费消息
- Boolean flag = customerData(msg);
- if (flag) {
- // 确定消息消费成功,应答ACK
- // 第一个参数:消息的唯一标识
- // 第二个参数:是否批量应答,一般都是false
- channel.basicAck(deliveryTag, false);
- System.out.println("消息成功,应答ACK");
- return;
- }
- System.out.println("消息过程中没有出现异常,但是消息没有消费成功,应答NACK");
- } catch (Exception e) {
- e.printStackTrace();
- System.out.println("消费过程中出现了异常,应答NACK");
- }
-
- // 消息消费失败,应答NACK
- // 第三个参数是:是否压入队列,如果设置为false该消息就丢弃了
- try {
- channel.basicNack(deliveryTag, false, true);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- // 完成消息的消费
- private Boolean customerData(String msg) {
- Integer i = Integer.parseInt(msg);
- if (i == 0) { // 没有插入成功,但是也没有出现异常
- return false;
- }
- return true;
- }
confirm机制是RabbitMQ自己提供的一个机制,用来确认消息是否到了交换机了。
reutrn机制是RabbitMQ自己提供的一个机制,用来确认消息是否到了队列了。
手动ACK后就明白这个消息已经被成功的消费了,可以从队列中删除了
- spring:
- rabbitmq:
- host: 192.168.127.102
- port: 5672
- username: guest
- password: guest
- virtual-host: /
- publisher-returns: true #开启return
- publisher-confirm-type: simple # 开启confirm
-
- /**
- * confirm机制和return机制
- */
- @Component
- public class MsgConfirm implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
-
- @Autowired
- private RabbitTemplate rabbitTemplate;
-
- @PostConstruct
- public void init() {
- //这里要对mq的return和confirm进行覆盖
- rabbitTemplate.setReturnCallback(this);
- rabbitTemplate.setConfirmCallback(this);
- }
-
- // confirm机制确认消息是否到了交换机
- @Override
- public void confirm(CorrelationData correlationData, boolean ack, String cause) {
- if (ack) {
- System.out.println("消息已经到了交换机");
- } else {
- System.out.println("消息没到了交换机");
- }
- }
-
- // 确认消息是否到了队列
- @Override
- public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
- System.out.println("消息没有到队列," + replyText + "," + exchange + "," + routingKey);
- }
生产者发送消息的时候除了交换机,数据,路由键把回调函数也发送过去了,因为消息是否到了交换机时MQServer确定的,如果到了就调用ACK
但是这只是通知消息是否正常到达,并没有对消息没有达到的情况进行一个处理,所以我们要进行消息补偿,生产者对未发送成功的消息进行一个消息补偿,特别是对业务很重要的数据必须补偿,无关紧要的倒可以不必因为消息补偿需要成本
也就是mq发送数据出现故障的时候,就可以考虑别的方式发送数据了,这就是消息补偿,如:RPC远程调用直接由生产者发送给消费者,不通过mq
- @Test
- void contextLoads2() throws Exception {
-
- // 这个类可以发送一个请求过去
- RestTemplate restTemplate = new RestTemplate();
-
- String info = restTemplate.getForObject("http://localhost:8080/send?msg=HTTP", String.class);
-
- System.out.println("远程调用的返回的结果:"+info);
- }
消息的重复消费指的是一个消息被同一个消费者消费了多次。
消费者拿到消息后干的事情是扣款或者是发送短信等待。
正常来说消费一次就够了,重复消费后就发现扣款了多次。
控制幂等性 幂等性:多次相同的操作不会对数据产生影响
接口的幂等性 就是多次相同的操作不会对数据再次改变
消息的幂等性 就是这个消息的重复消费不会对数据产生影响
3.为什么消息会被重复消费
消费者拿到数据开始消费,并且也消费成功了,在做ACK应答之前网络出现了闪断,消费者和MQServer断开了连接。MQServer中的待应答就会变成待分配,此时消息已经成功消费了,因为是闪断,所以又再次的连接成功,MQServer在再次的把消息推送给了消费者,消费者再次拿到数据再次进行消费,这里就出现了重复的消费。
这个地方只需要把消费者中调用消费数据的方法控制幂等性就可以了
Token机制:关于解决表单的重复提交就是服务端生成一个token带给表单,表单中隐藏这个token,多次提交携带token过去,第一次token有效进行操作,然后把token设为失效了,然后后面的提交携带的token就是失效的了,就不会操作了
CAS保证接口幂等性
乐观锁实现幂等性
防重表
缓存队列
select+insert
非正常的消息就是死信
被Nack的,被拒绝的,超过队列长度被挤出去的信息
保存死信的消息的队列就是死信队列
被nack的消息可以压入队列,然后在队列中配置了死信交换机和路由键
的信息,转到死信交换机,由死信交换机发给死信队列,然后交给另一个消费者处理
一般无法处理的消息都是死信,会把这些死信消息全部的转到同一个队列来做特殊的处理,这个队列就是死信队列。
有些场景不希望马上消费,是需要延时一段时间后再去消费。比如:10分钟后,半个小时后干点事情。
比如:订单超过关闭,验证码超时失效,这些都是延时任务,就可以通过延迟队列完成