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

http://127.0.0.1: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 实战的更多相关文章

  1. Java SpringBoot集成RabbitMq实战和总结

    目录 交换器.队列.绑定的声明 关于消息序列化 同一个队列多消费类型 注解将消息和消息头注入消费者方法 关于消费者确认 关于发送者确认模式 消费消息.死信队列和RetryTemplate RPC模式的 ...

  2. websocket+rabbitmq实战

    1. websocket+rabbitmq实战 1.1. 前言   接到的需求是后台定向给指定web登录用户推送消息,且可能同一账号会登录多个客户端都要接收到消息 1.2. 遇坑 基于springbo ...

  3. RabbitMQ实战应用技巧

    1. RabbitMQ实战应用技巧 1.1. 前言 由于项目原因,之后会和RabbitMQ比较多的打交道,所以让我们来好好整理下RabbitMQ的应用实战技巧,尽量避免日后的采坑 1.2. 概述 Ra ...

  4. Spring Boot 集成 RabbitMQ 实战

    Spring Boot 集成 RabbitMQ 实战 特别说明: 本文主要参考了程序员 DD 的博客文章<Spring Boot中使用RabbitMQ>,在此向原作者表示感谢. Mac 上 ...

  5. rabbitMQ实战(一)---------使用pika库实现hello world

    rabbitMQ实战(一)---------使用pika库实现hello world 2016-05-18 23:29 本站整理 浏览(267)     pika是RabbitMQ团队编写的官方Pyt ...

  6. celery+RabbitMQ 实战记录2—工程化使用

    上篇文章中,已经介绍了celery和RabbitMQ的安装以及基本用法. 本文将从工程的角度介绍如何使用celery. 1.配置和启动RabbitMQ 请参考celery+RabbitMQ实战记录. ...

  7. RabbitMQ实战经验分享

    前言 最近在忙一个高考项目,看着系统顺利完成了这次高考,终于可以松口气了.看到那些即将参加高考的学生,也想起当年高三的自己. 下面分享下RabbitMQ实战经验,希望对大家有所帮助: 一.生产消息 关 ...

  8. 【SpringBoot】Logback日志框架介绍和SpringBoot整合实战

    ========================11.Logback日志框架介绍和SpringBoot整合实战 2节课================================ 1.新日志框架L ...

  9. springboot+rabbitmq整合示例程

    关于什么是rabbitmq,请看另一篇文: http://www.cnblogs.com/boshen-hzb/p/6840064.html 一.新建maven工程:springboot-rabbit ...

  10. 【RabbitMQ 实战指南】一 RabbitMQ 开发

    1.RabbitMQ 安装 RabbitMQ 的安装可以参考官方文档:https://www.rabbitmq.com/download.html 2.管理页面 rabbitmq-management ...

随机推荐

  1. 介绍一款轻量型 Web SCADA 组态软件

    ​ 随着互联网.物联网技术的快速发展,图扑物联基于多年研发积累和私有部署实践打磨.以及对业务场景的深入理解,推出了适用于物联网应用场景的轻量型云组态软件. 该产品采用 B/S 架构,提供 Web 管理 ...

  2. jenkins pipeline语法、自动生成、部署案例

    Jenkins Pipeline是一套插件,支持在Jenkins中实现持续集成和持续交付: pipeline的编写都要写入到一个名为Jenkinsfile的文件中. 流水线脚本管理 Jenkinsfi ...

  3. 普冉PY32系列(十四) 从XL2400迁移到XL2400P

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...

  4. 8.解决elasticsearch深度分页问题

    前面说到,分页可以使用from和size参数,类似于mysql的分页offset和limit.但是如果数据量比较大时,elasticsearch会对分页做出限制,因为此时会比较消耗性能. 为什么要限制 ...

  5. startx详解

    linux下startx命令详解 用途 初始化一个 X 会话. 语法 startx [ -d Display:0 ] [ -t | -w ] [ -x Startup | [ -r Resources ...

  6. libGDX游戏开发之按轨迹移动(十一)

    libGDX游戏开发之运动轨迹绘制(十一) libGDX系列,游戏开发有unity3D巴拉巴拉的,为啥还用java开发?因为我是Java程序员emm-国内用libgdx比较少,多数情况需要去官网和go ...

  7. Zookeeper 的基本使用

    维基百科对 Zookeeper 的介绍如下所示: Apache ZooKeeper是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务.同步服务和命名注册 ZooKe ...

  8. Tpon 1.0 一键查询网站存在过的路径

    Tpon 1.0 寻找网站存在过的路径 该工具能够让你发现意料之外的路径 工具描述 编写该工具旨在寻找网站存在过的网站路径,这个地址可能是机器爬下来的也可能是某些人访问过的,在表面你可能看不到它的入口 ...

  9. 使用openfrp搭建网站[无公网ip]

    使用openfrp搭建网站的理由 免费/低成本 安全 可扩展 使用条件 有一台低功耗准系统/服务器[无公网ip] u盘 网线/waif网卡 屏幕 使用方法 第1步准备服务 低功耗准系统 / 服务器 推 ...

  10. Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战)

    Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战) 导入pom依赖 编写导出模板 @ApiOperation("导出xxx模板") @ ...