消息队列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)是一种应用间的通信 ...
随机推荐
- Jquery_002
6.$.ajax方法 $.ajax([options]) options是一个json格式的对象,参数是通过键值对的形式存在的 常用的参数如下: async:(默认: true) 默认设置下,所有请求 ...
- PHP myadmin 无路径getshell
PHP>5 & MySQl>5 环境:windows下常规的集成环境如 phpstudy,wamp,xampp等. 条件:当已经用弱口令或者爆破登录myadmin以后,没登录进入就 ...
- JAVA-批量下载zip
案例一 @ApiOperation(value = "根据id 批量下载文件", notes = "根据id 批量下载文件") @RequestMapping( ...
- Docker学习——Kubernetes(八)
在线阅读:GitBook 下载:pdf Kubernetes 是 Google 团队发起并维护的基于 Docker 的开源容器集群管理系统,它不仅支持常见的云平台,而且支持内部数据中心. 建于 Doc ...
- java 项目中Error linstenerStart 报错解决方法
项目中经常会遇到如下报错: 严重:Error linstenerStart 这种报错,我们看不出来到底是出现了什么问题.下面我们就一步一步来解决: (1)首先进入项目的classes目录下: (2)进 ...
- 问题:PHP扩展功能,去掉分号';'没有用,是怎么回事?(已解决)
1. 环境:win10的操作系统,IIS的服务器. 2. 问题描述:PHP要开启访问MYSQL的模块mysqli,我打开配置文件,去掉相关扩展模块前面的分号';',然后重启服务器,但是无效 ~~ 3. ...
- 网络IO模型_01
4种情况: 1.输入操作:等待数据到达套接字接收缓冲区: 2.输出操作:等待套接字发送缓冲区有足够的空间容纳将要发送的数据: 3.服务器接收连接请求:等待新的客户端连接请求的到来: 4.客户端发送连接 ...
- HDFS 机架感知与副本放置策略
HDFS 机架感知与副本放置策略 机架感知(RackAwareness) 通常,大型 Hadoop 集群会分布在很多机架上,在这种情况下, 希望不同节点之间的通信能够尽量发生在同一个机架之内,而不是跨 ...
- golang 生成Sqlserver数据表实体
最近开始学习golang,公司原来 很多项目都 Sqlserver数据库的,世面上很多文章,都是关于Mysql的,自己参考了一个博主的文章,整了一个生成Sqlserver实体的小工具分享一下,能给个星 ...
- Webpack解析与讲解
一.什么是Webpack? 一个基于node.js的前端模块化/预处理/扁平化处理器. 二.为什么要使用Webpack? 解决业务代码中的各种依赖,模块加载,静态文件引入问题(重复依赖/强依赖,阻塞加 ...