消息队列RabbitMQ业务场景应用及解决方案
0. 博客参考
- https://blog.csdn.net/weixin_42740268/article/details/84871509 使用docker第一次安装rabbitmq所踩过的坑
- https://www.cnblogs.com/geekdc/p/13604883.html 消费者消息确认的三种方式
- https://blog.csdn.net/qq_25933249/article/details/106868437 一文带你搞定RabbitMQ死信队列
- https://www.cnblogs.com/zhixie/p/12185574.html rabbitmq系列
1. 背景
需求说明:两个系统间需要数据互通,订单系统需要把一些订单信息、订单明细、回款、其他发送给B系统,但这些数据不是同时生成,还会有修改。直到订单的的状态改变为"审核通过",订单信息(所有的)才不会再继续推送。
两个系统是双向的,订单系统也会发送一些信息告诉B系统订单已完成/已取消,B系统也可以发送一些信息告诉订单系统订单已完成/已取消。从而促使对方的业务逻辑发生相应的变化。该篇文章假定为单向请求即订单系统向B系统发送数据
2. 技术选型
- 消息队列(rabbitMQ)
- 优点:异步,解耦(两个系统间)
- 缺点:需考虑在发送消息后每个节点出现异常报错的处理方法及消费者端发生异常报错的处理方法;此外还有消息堆积等问题
- 设想:在订单、回款、明细的add和edit方法中等待数据库事务操作成功后,异步发送消息给B系统
 
- 定时任务(xxl-job)
- 优点:有管理界面,每个微服务经过配置后在管理界面配置定时任务即可,后续可以方便修改时间,而无需在硬编码或在配置文件进行修改
- 缺点:无法获知数据什么时候发生了修改,只能定时从数据库凭状态判断,只要订单未完成/未取消就一直推送数据,同时还需要判断数据是新增/修改/删除
- 设想:和消息队列结合使用,将消费者这边未消费或消费失败的消息告知生产者或订单系统,使用定时任务去推送
 
- socket长连接或短连接
- 长连接:一有数据变化就进行推送,消费者消费后进行反馈,但比较消耗资源
- 短连接:一有数据变化就进行推送,消费者消费后进行反馈,但如果消费者处理消息报错或处理时间过长,则生产者无法判断是否消费成功
 
3. 消息队列的几个常见问题
- 生产者
- 消息是否发送到交换机
- 使用confirm机制告知生产者(事务也可以,但会降低效率(未测试过))
 
- 消息是否由交换机转发到队列
- 使用return机制告知生产者
 
 
- 消息是否发送到交换机
- 消费者
- 消费者是否接收到消息
- 使用手动确认的方式 ack/nack
 
- 使用手动确认的方式 
- 如果未接收到消息,是否重试?重试几次?时间间隔多久?如果重试失败该如何处理
- 在application.properties/yml配置rabbitmq的retry参数
 
- 如果保证消息的幂等性(即针对消息重复推送如何只消费一条消息)
- 生产者发送消息是传一个messageId(UUID),消费者在消费时使用缓存redis存储,如果第二次传过来的还是这个,则跳过
 
- 如果消费失败,如果把消息转入死信队列
- 配置相应的死信交换机和死信队列,对于业务队列配置相应的参数,使得消息在被拒绝时跳转至死信交换机和死信队列,供死信消费者处理(获得消息后根据业务来处理,是入库还是推送给生产者等等)
 
 
- 消费者是否接收到消息
4. 代码功能开发及测试
首先,创建两个demo,分别叫做rabbit-producer和rabbit-consumer。两个demo的项目架构如下:


pom.xml内容如下:
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-rest</artifactId>
	</dependency>
	<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>
	<!--redis用于处理消息的幂等性-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<!--如果是消息是否有问题,可以发邮件给开发人员进行通知-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-mail</artifactId>
	</dependency>
</dependencies>
consumer.yml内容如下:
spring:
  application:
    # 应用名称
    name: rabbit-consumer
  redis:
    host: 127.0.0.1
    port: 6379
    password:
  rabbitmq:
    # 连接地址
    host: 127.0.0.1
    # 端口
    port: 5672
    # 登录账号
    username: guest
    # 登录密码
    password: guest
    # 虚拟主机
    virtual-host: /
    listener:
      simple:
        #手动签收消息
        acknowledge-mode: manual
        # 投递失败时是否重新排队 默认值:true
        default-requeue-rejected: false
        retry:
          enabled: true # 开启消费者进行重试
          max-attempts: 5 # 最大重试次数
          initial-interval: 3000 # 重试时间间隔
producer.yml内容如下:
spring:
  application:
    # 应用名称
    name: rabbit-producer
  redis:
    host: 127.0.0.1
    port: 6379
    password:
  rabbitmq:
    # 连接地址
    host: 127.0.0.1
    # 端口
    port: 5672
    # 登录账号
    username: guest
    # 登录密码
    password: guest
    # 虚拟主机
    virtual-host: /
    #开启生产者确认机制,是否到达交换机,也可以填sample
    publisher-confirm-type: correlated
    #交换机是否到达队列
    publisher-returns: true
    #消息是否到达交换机
    publisher-confirms: true
    listener:
      simple:
        acknowledge-mode: manual
        # 投递失败时是否重新排队 默认值:true
        default-requeue-rejected: false
4.1 生产者
生产者主要由一个配置类RabbitConfig和一个Controller组成,配置类用于创建交换机、队列和配置绑定关系等。生产者用于发送消息,确认消息是否到达
package com.example.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.SerializerMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitConfig {
    //业务交换机
    public static final String ORDER_EXCHANGE = "order_exchange";
    //死信交换机
    public static final String DEAD_LETTER_EXCHANGE = "order_exchange_dead_letter";
    //业务队列
    public static final String ORDER_QUEUE = "order_queue";
    //死信队列
    public static final String DEAD_LETTER_ORDER_QUEUE = "order_queue_dead_letter";
    //路由
    public static final String ROUTING_KEY_QUEUE_ORDER = "key_order";
    public static final String DEAD_LETTER_ROUTING_KEY_QUEUE_ORDER = "key_order_dead_letter";
    @Bean
    public DirectExchange orderExchange(){
        return new DirectExchange(ORDER_EXCHANGE,true,false);
    }
    @Bean
    public DirectExchange deadLetterExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE,true,false);
    }
    @Bean
    public Queue orderQueue(){
        Map<String, Object> args = new HashMap<>(2);
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        args.put("x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY_QUEUE_ORDER);
        return new Queue(ORDER_QUEUE,true,false,false,args);
    }
    @Bean
    public Queue deadLetterOrderqueue(){
        return new Queue(DEAD_LETTER_ORDER_QUEUE,true);
    }
    @Bean
    public Queue businessQueueA(){
        Map<String, Object> args = new HashMap<>(2);
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        args.put("x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY_QUEUE_ORDER);
        return QueueBuilder.durable(ORDER_QUEUE).withArguments(args).build();
    }
    @Bean
    public Binding orderBinding(){
        return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(ROUTING_KEY_QUEUE_ORDER);
    }
    @Bean
    public Binding orderDeadLetterBinding(){
        return BindingBuilder.bind(deadLetterOrderqueue()).to(deadLetterExchange()).with(DEAD_LETTER_ROUTING_KEY_QUEUE_ORDER);
    }
    // java.lang.IllegalStateException: Only one ConfirmCallback is supported by each RabbitTemplate
    @Bean
    @Scope("prototype")
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMandatory(true);
        template.setMessageConverter(new SerializerMessageConverter());
        return template;
    }
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }
}
package com.example.demo.controller;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/rabbitProducer")
public class Producer {
    //业务交换机
    public static final String ORDER_EXCHANGE = "order_exchange";
    public static final String ROUTING_KEY_QUEUE_ORDER = "key_order";
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @GetMapping("/send")
    public void sendMessage(){
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("email","11111111111");
        jsonObject.put("timestamp",System.currentTimeMillis());
        String json = jsonObject.toJSONString();
        Message message = MessageBuilder.withBody(json.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .setContentEncoding("UTF-8").setMessageId(UUID.randomUUID()+"").build();
        System.out.println(json);
        /**
         * 消息是否到达交换机
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    log.info("发送消息到交换器成功");
                }else{
                    log.info("发送消息到交换器失败");
                }
                System.out.println(correlationData);
                System.out.println("发送消息到交换器标志(true-成功 false-失败): "+ack);
                System.out.println(cause);
            }
        });
        /**
         * 消息是否达到队列
         */
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("------------- 没到达队列 --------------");
                System.out.println(returnedMessage);
                System.out.println("------------- 没到达队列 --------------");
            }
        });
        //
        rabbitTemplate.convertAndSend(ORDER_EXCHANGE,ROUTING_KEY_QUEUE_ORDER,message);
    }
}
4.2 消费者
package com.example.demo.controller;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@Slf4j
@Component
public class Consumer {
    //业务队列
    public static final String ORDER_QUEUE = "order_queue";
    //死信队列
    public static final String DEAD_LETTER_ORDER_QUEUE = "order_queue_dead_letter";
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    private JavaMailSender mailSender;
    @RabbitListener(queues = ORDER_QUEUE)
    @RabbitHandler
    public void receiveMessage(Message message, Channel channel) throws IOException {
        try{
			//用于测试是否会进入死信队列被消费
            int x = 1 / 0;
            String messageId = message.getMessageProperties().getMessageId();
            String msg = new String(message.getBody(),"UTF-8");
            System.out.println("接收导的消息为:"+msg+"==消息id为:"+messageId);
            String messageIdRedis = null;
            //验证是否是重复消息
            if(redisTemplate.hasKey("messageId")){
                messageIdRedis = redisTemplate.opsForValue().get("messageId").toString();
                if(messageId.equals(messageIdRedis)){
                    //说明消息已被消费
                    return;
                }
            }
            redisTemplate.opsForValue().set("messageId",messageId);
            System.out.println("-----------------------------------------------------------");
            System.out.println("接收到的消息为"+msg);
            System.out.println("-----------------------------------------------------------");
            //手动签收
            //给接收到消息打个标记。默认应由RabbitMQ随机生成并用来它自己区分接收到的消息。所以此处应赋值为message.getMessageProperties().getDeliveryTag()
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            System.out.println(deliveryTag);
            //可以做一些确认,比如code=200,才手动确认
            channel.basicAck(deliveryTag,false);
            // 第二个参数是否批量确认,第三个参数是否重新回队列
            //channel.basicNack(deliveryTag,false,true);
        }catch (Exception e){
          /*  SimpleMailMessage mailMsg = new SimpleMailMessage();
            // 发件人
            mailMsg.setFrom("hexiangli@chosenmedtech.com");
            // 收件人
            mailMsg.setTo("hexiangli@chosenmedtech.com");
            // 邮件标题
            mailMsg.setSubject("消息队列异常,请及时解决");
            // 邮件内容
            mailMsg.setText("crm与limis消息队列消费异常");
            // 抄送人
            mailMsg.setCc("2393545826@qq.com");
            mailSender.send(mailMsg);*/
            log.error("消息消费发生异常,error msg:{}", e.getMessage());
            channel.basicNack((Long)message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
   @RabbitListener(queues = DEAD_LETTER_ORDER_QUEUE)
    public void receiveB(Message message, Channel channel) throws IOException {
        System.out.println("收到死信消息B:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
5. 源代码
https://gitee.com/lhx890/rabbitmq-demo.git
6.补充:消息的顺序性
比如有关数据库操作,新增/修改/删除 或者 新增/删除/新增/修改,如果顺序错了,数据库操作也将失败。如果对于同一个订单进行数据库操作需保持它的顺序性。即把消息推送到同一个queue,一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
参考:https://zhuanlan.zhihu.com/p/60166828
消息队列RabbitMQ业务场景应用及解决方案的更多相关文章
- RabbitMQ入门教程(十七):消息队列的应用场景和常见的消息队列之间的比较
		原文:RabbitMQ入门教程(十七):消息队列的应用场景和常见的消息队列之间的比较 分享一个朋友的人工智能教程.比较通俗易懂,风趣幽默,感兴趣的朋友可以去看看. 这是网上的一篇教程写的很好,不知原作 ... 
- 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化
		前文目录链接参考: 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16694126.html 消息队列 ... 
- (二)RabbitMQ消息队列-RabbitMQ消息队列架构与基本概念
		原文:(二)RabbitMQ消息队列-RabbitMQ消息队列架构与基本概念 没错我还是没有讲怎么安装和写一个HelloWord,不过快了,这一章我们先了解下RabbitMQ的基本概念. Rabbit ... 
- ASP.NET Core消息队列RabbitMQ基础入门实战演练
		一.课程介绍 人生苦短,我用.NET Core!消息队列RabbitMQ大家相比都不陌生,本次分享课程阿笨将给大家分享一下在一般项目中99%都会用到的消息队列MQ的一个实战业务运用场景.本次分享课程不 ... 
- 消息队列rabbitmq/kafka
		12.1 rabbitMQ 1. 你了解的消息队列 rabbitmq是一个消息代理,它接收和转发消息,可以理解为是生活的邮局.你可以将邮件放在邮箱里,你可以确定有邮递员会发送邮件给收件人.概括:rab ... 
- 消息队列rabbitmq  rabbitMQ安装
		消息队列rabbitmq 12.1 rabbitMQ 1. 你了解的消息队列 生活里的消息队列,如同邮局的邮箱, 如果没邮箱的话, 邮件必须找到邮件那个人,递给他,才玩完成,那这个任务会处理的很麻 ... 
- .NET 开源工作流: Slickflow流程引擎高级开发(七)--消息队列(RabbitMQ)的集成使用
		前言:工作流流程过程中,除了正常的人工审批类型的节点外,事件类型的节点处理也尤为重要.比如比较常见的事件类型的节点有:Timer/Message/Signal等.本文重点阐述消息类型的节点处理,以及实 ... 
- openstack (共享服务) 消息队列rabbitmq服务
		云计算openstack共享组件——消息队列rabbitmq(3) 一.MQ 全称为 Message Queue, 消息队列( MQ ) 是一种应用程序对应用程序的通信方法.应用程序通过读写出入队 ... 
- 消息队列的使用场景(转载c)
		作者:ScienJus链接:https://www.zhihu.com/question/34243607/answer/58314162来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业 ... 
- 架构设计之NodeJS操作消息队列RabbitMQ
		一. 什么是消息队列? 消息(Message)是指在应用间传送的数据.消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象. 消息队列(Message Queue)是一种应用间的通信 ... 
随机推荐
- B - WeirdSort
			B - WeirdSort 思路:经过认真的审题,你会发现,这只是个冒泡的变形,我们建立两个数组,然后用一个数组里面的数字确定位置,然后冒泡就行了.最后抖机灵用了个is_sorted,判断数组里面数字 ... 
- <鸳鸯刀>&<白马啸西风>随笔
			这两部作品比较小众,也不如之前的作品优秀,因此简单写一下好了. <鸳鸯刀> 陕西西安府威信镖局的总镖头."铁鞭镇八方"周威信,带领一支七十多人的镖队正前往京城.路途之上 ... 
- b站——沐神——深度学习
			预备知识 数据操作 MXNet nd:(array函数:得到NDArray) [[1. 1. 1.] [1. 1. 1.]] <NDArray 2x3 @cpu(0)> np:(asnum ... 
- kubectl使用方法及常用命令小结
			Kubectl 是一个命令行接口,用于对 Kubernetes 集群运行命令.kubectl 在 $HOME/.kube 目录中寻找一个名为 config 的文件. kubectl安装方法详见:htt ... 
- C#访问MySQL(二):数据插入与修改(增改)
			前言: 前面说了数据库的连接查询,现在说数据库的增删改.这里引入一个数据库的实体类,就是将当前数据库的某一个表里面所有字段写成实体类,如下: 1.数据库的实体类: 需要项目里下载Chloe.dll和C ... 
- 【redis-cli】常用命令
			-- 查看数据库下key存储数 redis-cli INFO | grep ^db -- 选择数据库 select [0-15] -- 列出所有key keys * -- 列出key模糊匹配 keys ... 
- Jmeter三、重要组件(元素)介绍
			一.组件 1.sampler 2.计时器timer 3.(sampler的)前置处理器pre processors, 后置处理器post processors 4.断言assertion==loadr ... 
- Activity基础知识
			Activity 一.Activity是什么 Activity是一种可以包含用户界面的组件,主要用于和用户进行交互.一个应用程序可以包含零个或多个活动. 二.活动的基本用法 1. 手动创建活动  打 ... 
- WEB应用中配置和使用springIOC容器是成功的
			Sring web应用学习(1)https://www.cnblogs.com/xiximayou/p/12172667.html 
- mysql和nacos都部署在docker中,ip该写哪个
			docker run -d \ -e MODE=standalone \ -e SPRING_DATASOURCE_PLATFORM=mysql \ -e MYSQL_SERVICE_HOST=172 ... 
