SpringBoot RabbitMQ 实战

2022-06-24 15:13:14 浏览数 (2)

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.编译依赖

代码语言:javascript复制
yum install esl-erlang_23.0-1_centos_7_amd64.rpm

2.查看Erlang版

代码语言:javascript复制
erl -version
二、安装RabbitMq

1.解压包

代码语言:javascript复制
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

代码语言:javascript复制
cd rabbitmq_server-3.7.7/sbin

export PATH=/usr/local/rabbitmq_server-3.7.7/sbin:$PATH

3.PATH路径更新

代码语言:javascript复制
source /etc/profile

4.开启 web 管理插件

代码语言:javascript复制
rabbitmq-plugins enable rabbitmq_management

5.启动

代码语言:javascript复制
#启动后台管理
./rabbitmq-plugins enable rabbitmq_management   

#后台运行rabbitmq
./rabbitmq-server -detached

6.新建用户

由于guest用户被限制,只能通过localhost访问,因此我们需要新建一个用户,并授予管理员权限。

新建一个用户名为admin,密码为admin的用户,并授予管理员(administrator)权限

代码语言:javascript复制
./rabbitmqctl add_user admin admin

./rabbitmqctl set_user_tags admin administrator

7.浏览器访问

默认端口为:15672

http://127.0.0.1:15672

效果

SpringBoot整合

以下贴出的代码只是关键代码,涉及业务相关的已删除,请自行处理。

1、说明

使用是主题模式 主题交换机 消息确认机制

2、添加依赖
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3、核心配置文件
代码语言:javascript复制
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)
代码语言:javascript复制
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
代码语言:javascript复制
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

代码语言:javascript复制
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

代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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;
  • 如果存在,则证明消息被消费过,直接丢弃。

0 人点赞