SpringBoot RabbitMQ 实战
RabbitMQ
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
RabbitMQ是一套开源(MPL)的消息队列服务软件,是由 LShift 提供的一个 Advanced Message Queuing Protocol (AMQP) 的开源实现,由以高性能、健壮以及可伸缩性出名的 Erlang 写成。
RabbitMQ 七种队列模式
- 简单模式(Hello World):
做最简单的事情,一个生产者对应一个消费者,RabbitMQ相当于一个消息代理,负责将A的消息转发给B。 - 工作队列模式(Work queues):
在多个消费者之间分配任务(竞争的消费者模式),一个生产者对应多个消费者,一般适用于执行资源密集型任务,单个消费者处理不过来,需要多个消费者进行处理。 - 订阅模式(Publish/Subscribe):
一次向许多消费者发送消息,一个生产者发送的消息会被多个消费者获取,也就是将消息将广播到所有的消费者中。 - 路由模式(Routing):
有选择地(Routing key)接收消息,发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key,仅消费指定路由key的消息。 - 主题模式(Topics):
根据主题(Topics)来接收消息,将路由key和某模式进行匹配,此时队列需要绑定在一个模式上,#匹配一个词或多个词,*只匹配一个词。 - 远程过程调用(RPC):客户端发送一个请求消息,服务端以一个响应消息回应。
- 发布者确认(Publisher Confirms):与发布者进行可靠的发布确认,发布者确认是RabbitMQ扩展,可以实现可靠的发布。在通道上启用发布者确认后,RabbitMQ将异步确认发送者发布的消息,这意味着它们已在服务器端处理。
四种交换机
- 直连交换机(Direct exchange):
具有路由功能的交换机,绑定到此交换机的时候需要指定一个routing_key,交换机发送消息的时候需要routing_key,会将消息发送道对应的队列。 - 扇形交换机(Fanout exchange):
广播消息到所有队列,没有任何处理,速度最快。 - 主题交换机(Topic exchange):
在直连交换机基础上增加模式匹配,也就是对routing_key进行模式匹配,*代表一个单词,#代表多个单词。 - 首部交换机(Headers exchange):
忽略routing_key,使用Headers信息(一个Hash的数据结构)进行匹配,优势在于可以有更多更灵活的匹配规则。
安装环境(centos7)
安装包下载
Erlang和RabbitMQ官网下载速度特别慢,可用 erlang-solutions.com 下载,速度特别快。
https://www.erlang-solutions.com/downloads/
安装版本
Erlang:esl-erlang_23.0-1_centos_7_amd64.rpm
RabbitMQ:rabbitmq-server-generic-unix-3.7.7.tar.xz
一、安装Erlang
1.编译依赖
yum install esl-erlang_23.0-1_centos_7_amd64.rpm
2.查看Erlang版
erl -version
二、安装RabbitMq
1.解压包
xz -d rabbitmq-server-generic-unix-3.7.7.tar.xz
tar -xvf rabbitmq-server-generic-unix-3.7.7.tar
2.设置RabbitMq的环境变量
进入到rabbit文件内,其命令文件存在于sbin文件夹下,因此需要将sbin文件夹的路径添加到PATH中:修改/etc/profile
cd rabbitmq_server-3.7.7/sbin
export PATH=/usr/local/rabbitmq_server-3.7.7/sbin:$PATH
3.PATH路径更新
source /etc/profile
4.开启 web 管理插件
rabbitmq-plugins enable rabbitmq_management
5.启动
#启动后台管理
./rabbitmq-plugins enable rabbitmq_management
#后台运行rabbitmq
./rabbitmq-server -detached
6.新建用户
由于guest用户被限制,只能通过localhost访问,因此我们需要新建一个用户,并授予管理员权限。
新建一个用户名为admin,密码为admin的用户,并授予管理员(administrator)权限
./rabbitmqctl add_user admin admin
./rabbitmqctl set_user_tags admin administrator
7.浏览器访问
默认端口为:15672
效果
SpringBoot整合
以下贴出的代码只是关键代码,涉及业务相关的已删除,请自行处理。
1、说明
使用是主题模式 + 主题交换机 + 消息确认机制
2、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3、核心配置文件
spring:
# 配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
# 开启confirm确认机制
publisher-confirms: true
# 开启return确认机制
publisher-returns: true
template:
# 设置为true后,消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
mandatory: true
listener:
simple:
# 设置消费端手动 ack
acknowledge-mode: manual
# 消费者最小数量
concurrency: 1
# 消费之最大数量
max-concurrency: 20
# 每次只处理一个消息
prefetch: 1
retry:
# 是否支持重试
enabled: true
4、常量配置(AmqpConstant.java)
public class AmqpConstant {
/**
* 支付路由key
* *:匹配不多不少一个词
* #:匹配一个或多个词
*/
public static final String PAY_QUEUE = "pay_queue";
public static final String PAY_ROUTING_KEY = "pay.order";
public static final String _PAY_ROUTING_KEY = "pay.#";
/**
* 查询路由key
*/
public static final String QUERY_QUEUE = "query_queue";
public static final String QUERY_ROUTING_KEY = "query.order";
public static final String _QUERY_ROUTING_KEY = "query.#";
/**
* 交换机名称
*/
public static final String PAY_EXCHANGE_NAME = "PAY-EXCHANGE";
//===================================================================================
/**
* 使用Redis保证消息幂等性
*/
public static final String PAY_QUEUE_REDIS_KEY = "pay-queue:";
/**
* Redis消息ID key 的过期时间,单位:秒
*/
public static final Integer PAY_QUEUE_REDIS_KEY_TIMEOUT = 7200;
/**
* 轮询查询订单状态的睡眠时间,单位:秒
*/
public static final Integer POLLING_STATUS_TIME = 2000;
}
5、配置队列、路由交换机,TopicQueueConfig.java
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class TopicQueueConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 支付队列
*
* @return
*/
@Bean
public Queue payTopicQueue() {
return new Queue(AmqpConstant.PAY_QUEUE, true);
}
/**
* 查询队列
*
* @return
*/
@Bean
public Queue queryTopicQueue() {
return new Queue(AmqpConstant.QUERY_QUEUE, true);
}
/**
* 路由交换机
*
* @return
*/
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(AmqpConstant.PAY_EXCHANGE_NAME);
}
/**
* 队列绑定交换机,指定routingKey,也可在可视化工具中进行绑定
*
* @return
*/
@Bean
Binding payBindingTopicExchange(Queue payTopicQueue, TopicExchange exchange) {
return BindingBuilder.bind(payTopicQueue).to(exchange).with(AmqpConstant._PAY_ROUTING_KEY);
}
/**
* 队列绑定交换机,指定routingKey,也可在可视化工具中进行绑定
*
* @return
*/
@Bean
Binding queryBindingTopicExchange(Queue queryTopicQueue, TopicExchange exchange) {
return BindingBuilder.bind(queryTopicQueue).to(exchange).with(AmqpConstant._QUERY_ROUTING_KEY);
}
@PostConstruct
public void initRabbitTemplate() {
// 设置生产者消息确认
rabbitTemplate.setConfirmCallback(new RabbitConfirmCallback());
rabbitTemplate.setReturnCallback(new RabbitConfirmReturnCallBack());
}
}
6、生产端 Confirm 消息确认机制
消息的确认,是指生产者投递消息后,如果 Broker 收到消息,则会给我们生产者一个应答。生产者进行接收应答,用来确定这条消息是否正常的发送到 Broker ,这种方式也是消息的可靠性投递的核心保障!
RabbitConfirmCallback.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* 消息发送确认类
*/
@Slf4j
public class RabbitConfirmCallback implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("=======ConfirmCallback===correlationData:{}=====ack:{}=====cause:{}========", correlationData, ack, cause);
}
}
7、Return 消息机制
Return Listener 用于处理一-些不可路 由的消息!
当exchange不存在或者指定的路由 key 路由不到时,这个时候如果我们需要监听这种不可达的消息,就要使用 Return Listener !
RabbitConfirmReturnCallBack.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* 消息发送交换机返回机制
*/
@Slf4j
public class RabbitConfirmReturnCallBack implements RabbitTemplate.ReturnCallback{
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("===RabbitConfirmReturnCallBack====exchange: {}======routingKey:{}=====replyCode:{}====replyText: {}======", exchange, routingKey, replyCode, replyText);
}
}
在基础API中有一个关键的配置项:Mandatory:如果为 true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为 false,那么 broker 端自动删除该消息!
8、消息生产者相关
一个发送支付生产者、一个查询订单转态的生产者。
8.1、生产者 OrderProvider.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
/**
* 消息生产者
*/
@Slf4j
@Service
public class OrderProvider {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 支付消息生产者方法
*
* @param messages
*/
public void sendObject(List<Message> messages) {
try {
if (!Objects.isNull(messages) && messages.size() > 0) {
log.info("========支付生产消息开始=====总消息个数为:{}==========", messages.size());
messages.forEach(message -> {
// 全局唯一
CorrelationData correlationData = new CorrelationData(message.getMessageId());
rabbitTemplate.convertAndSend(AmqpConstant.PAY_EXCHANGE_NAME, AmqpConstant.PAY_ROUTING_KEY, message, correlationData);
});
log.info("========支付生产消息结束=====总消息个数为:{}==========", messages.size());
}
} catch (Exception e) {
log.error("====支付生产消息异常=======:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 查询消息生产者方法
*
* @param message
*/
public void sendQuery(Message message) {
try {
if (Objects.nonNull(message)) {
log.info("========查询消息生产者开始=====");
// 全局唯一
CorrelationData correlationData = new CorrelationData(message.getMessageId());
rabbitTemplate.convertAndSend(AmqpConstant.PAY_EXCHANGE_NAME, AmqpConstant.QUERY_ROUTING_KEY, message, correlationData);
log.info("========查询消息生产者结束=======消息ID:{}", correlationData);
}
} catch (Exception e) {
log.error("====查询消息生产者异常=======:{}", e.getMessage());
e.printStackTrace();
}
}
}
8.2、消息实体 Message.java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class Message implements Serializable {
/**
* 消息Id
*/
private String messageId;
/**
* 消息内容
*/
private Object messageData;
/**
* 消息创建时间
*/
private LocalDateTime createTime;
}
8.3、生产者Controller OrderProviderController.java
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RestController
@RequestMapping(value = "/mqprovider")
@AllArgsConstructor
public class OrderProviderController {
@Autowired
private OrderProvider orderProvider;
/**
*支付消息生产者
*/
@PostMapping("/sendobject")
public String sendobject(@RequestBody List<Message> messageList) {
orderProvider.sendObject(messageList);
return "success";
}
/**
*查询消息生产者
*/
@PostMapping("/sendquery")
public String sendQuery(@RequestBody Message message) {
orderProvider.sendQuery(message);
return "success";
}
}
9、消费者相关
9.1、支付消息接收监听 OrderPayReceiver.java
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 支付消息接收监听
*/
@Slf4j
@Component
public class OrderPayReceiver {
@Autowired
private OrderProvider orderProvider;
@Autowired
private RedisTemplate redisTemplate;
@RabbitListener(queues = AmqpConstant.PAY_QUEUE)
public void payMessage(@Payload Message message, Channel channel, @Headers Map<String, Object> headers) {
try {
// 保证消息幂等性
Object redisMessageId = redisTemplate.opsForValue().get(AmqpConstant.PAY_QUEUE_REDIS_KEY + message.getMessageId());
if (!Objects.isNull(redisMessageId) && message.getMessageId().equals(String.valueOf(redisMessageId))) {
// 手工ack
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), true);
return;
}
log.info("==============支付消费者开始======消息ID:{}====消息创建时间:{}", message.getMessageId(), message.getCreateTime());
// 调用支付接口 TODO
// 消息ID存入Redis
redisTemplate.opsForValue().set(AmqpConstant.PAY_QUEUE_REDIS_KEY + message.getMessageId(), message.getMessageId(), AmqpConstant.PAY_QUEUE_REDIS_KEY_TIMEOUT, TimeUnit.SECONDS);
// 将支付调用成功的消息放入查询队列中 TODO
// orderProvider.sendQuery();
// 手工ack
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), true);
log.info("==============支付消费者结束==========");
} catch (Exception e) {
log.error("==============支付消费者异常,异常信息:{}", e.getMessage());
e.printStackTrace();
}
}
}
9.2、查询消息接收监听 OrderQueryReceiver.java
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Objects;
/**
* 查询消息接收监听
*/
@Slf4j
@Component
public class OrderQueryReceiver {
@RabbitListener(queues = AmqpConstant.QUERY_QUEUE)
public void onUserMessage(@Payload Message message, Channel channel, @Headers Map<String, Object> headers) {
try {
log.info("==============查询消费者开始======消息ID:{}====消息创建时间:{}", message.getMessageId(), message.getCreateTime());
// 调用查询订单状态接口 TODO
log.info("=====查询支付接口返回======支付状态:{}====支付状态描述:{}=====", result.getCode(), result.getMessage());
// 手工ack
long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(deliveryTag, true);
log.info("==============查询消费者结束======deliveryTag:{}====", deliveryTag);
} catch (Exception e) {
log.error("==============查询消费者异常,异常信息:{}", e.getMessage());
e.printStackTrace();
}
}
}
业务架构
需求:每天按照需求批量生产订单数据,进行调用三方支付接口进行支付操作。有实时放回支付状态的,也有异步返回状态的,具需求要求本业务不提供回调更新状态机制,需自身请求三方接口更新订单转态(业务中有批量支付操作)。
简易业务架构图如下:
以上是结合项目需求设计的业务实现。
如何保证消息不被重复消费?
生产时消息重复
为什么会出现消息重复?消息重复的原因有两个:
1.生产时消息重复,
2.消费时消息重复。
由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。
生产者中如果消息未被确认,或确认失败,可以使用定时任务+(redis/db)来进行消息重试。
消费时消息重复
以订单ID作为消息ID,即可保证消息的幂等性,消费过程为:
消费者获取到消息后先根据id去查询redis/db是否存在该消息;
如果不存在,则正常消费,消费完毕后写入redis/db;
如果存在,则证明消息被消费过,直接丢弃。
SpringBoot RabbitMQ 实战的更多相关文章
- Java SpringBoot集成RabbitMq实战和总结
目录 交换器.队列.绑定的声明 关于消息序列化 同一个队列多消费类型 注解将消息和消息头注入消费者方法 关于消费者确认 关于发送者确认模式 消费消息.死信队列和RetryTemplate RPC模式的 ...
- websocket+rabbitmq实战
1. websocket+rabbitmq实战 1.1. 前言 接到的需求是后台定向给指定web登录用户推送消息,且可能同一账号会登录多个客户端都要接收到消息 1.2. 遇坑 基于springbo ...
- RabbitMQ实战应用技巧
1. RabbitMQ实战应用技巧 1.1. 前言 由于项目原因,之后会和RabbitMQ比较多的打交道,所以让我们来好好整理下RabbitMQ的应用实战技巧,尽量避免日后的采坑 1.2. 概述 Ra ...
- Spring Boot 集成 RabbitMQ 实战
Spring Boot 集成 RabbitMQ 实战 特别说明: 本文主要参考了程序员 DD 的博客文章<Spring Boot中使用RabbitMQ>,在此向原作者表示感谢. Mac 上 ...
- rabbitMQ实战(一)---------使用pika库实现hello world
rabbitMQ实战(一)---------使用pika库实现hello world 2016-05-18 23:29 本站整理 浏览(267) pika是RabbitMQ团队编写的官方Pyt ...
- celery+RabbitMQ 实战记录2—工程化使用
上篇文章中,已经介绍了celery和RabbitMQ的安装以及基本用法. 本文将从工程的角度介绍如何使用celery. 1.配置和启动RabbitMQ 请参考celery+RabbitMQ实战记录. ...
- RabbitMQ实战经验分享
前言 最近在忙一个高考项目,看着系统顺利完成了这次高考,终于可以松口气了.看到那些即将参加高考的学生,也想起当年高三的自己. 下面分享下RabbitMQ实战经验,希望对大家有所帮助: 一.生产消息 关 ...
- 【SpringBoot】Logback日志框架介绍和SpringBoot整合实战
========================11.Logback日志框架介绍和SpringBoot整合实战 2节课================================ 1.新日志框架L ...
- springboot+rabbitmq整合示例程
关于什么是rabbitmq,请看另一篇文: http://www.cnblogs.com/boshen-hzb/p/6840064.html 一.新建maven工程:springboot-rabbit ...
- 【RabbitMQ 实战指南】一 RabbitMQ 开发
1.RabbitMQ 安装 RabbitMQ 的安装可以参考官方文档:https://www.rabbitmq.com/download.html 2.管理页面 rabbitmq-management ...
随机推荐
- 介绍一款轻量型 Web SCADA 组态软件
随着互联网.物联网技术的快速发展,图扑物联基于多年研发积累和私有部署实践打磨.以及对业务场景的深入理解,推出了适用于物联网应用场景的轻量型云组态软件. 该产品采用 B/S 架构,提供 Web 管理 ...
- jenkins pipeline语法、自动生成、部署案例
Jenkins Pipeline是一套插件,支持在Jenkins中实现持续集成和持续交付: pipeline的编写都要写入到一个名为Jenkinsfile的文件中. 流水线脚本管理 Jenkinsfi ...
- 普冉PY32系列(十四) 从XL2400迁移到XL2400P
目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...
- 8.解决elasticsearch深度分页问题
前面说到,分页可以使用from和size参数,类似于mysql的分页offset和limit.但是如果数据量比较大时,elasticsearch会对分页做出限制,因为此时会比较消耗性能. 为什么要限制 ...
- startx详解
linux下startx命令详解 用途 初始化一个 X 会话. 语法 startx [ -d Display:0 ] [ -t | -w ] [ -x Startup | [ -r Resources ...
- libGDX游戏开发之按轨迹移动(十一)
libGDX游戏开发之运动轨迹绘制(十一) libGDX系列,游戏开发有unity3D巴拉巴拉的,为啥还用java开发?因为我是Java程序员emm-国内用libgdx比较少,多数情况需要去官网和go ...
- Zookeeper 的基本使用
维基百科对 Zookeeper 的介绍如下所示: Apache ZooKeeper是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务.同步服务和命名注册 ZooKe ...
- Tpon 1.0 一键查询网站存在过的路径
Tpon 1.0 寻找网站存在过的路径 该工具能够让你发现意料之外的路径 工具描述 编写该工具旨在寻找网站存在过的网站路径,这个地址可能是机器爬下来的也可能是某些人访问过的,在表面你可能看不到它的入口 ...
- 使用openfrp搭建网站[无公网ip]
使用openfrp搭建网站的理由 免费/低成本 安全 可扩展 使用条件 有一台低功耗准系统/服务器[无公网ip] u盘 网线/waif网卡 屏幕 使用方法 第1步准备服务 低功耗准系统 / 服务器 推 ...
- Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战)
Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战) 导入pom依赖 编写导出模板 @ApiOperation("导出xxx模板") @ ...