MQ(IBM MQ)代表消息队列,是一种应用程序对应用程序的通信方法;
通过消息传递队列发送和接收消息数据,支持应用程序,系统,服务和文件之间的信息交换。
这简化了业务应用程序的创建和维护。
RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用。
RabbitMQ中的交换机有Direct Exchange(直连交换机)、Topic Exchange(主题交换器)、Fanout Exchange(广播式交换机)、Headers Exchange(Headers交换机)四种,常用的就前三种,本文将对直连交换机进行演示,并加入消息确认机制以及整合redis防止重复消费问题。
docker 方式安装
# 拉取镜像
docker pull rabbitmq:management
# 创建容器并运行
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 rabbitmq:management
访问http://ip:15672,RabbitMQ默认的用户名:guest,密码:guest
具体信息不在介绍,我们直接创建项目使用
首先创建一个rabbitmq-producer生成者springboot项目添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
创建RabbitMQConfig.java配置类
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
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;
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_A = "exchange-A";
public static final String QUEUE_A = "queue-a";
public static final String ROUTINGKEY_A = "routing-key-A";
/**
* 设置交换机
*/
@Bean
public DirectExchange exchangeA() {
return new DirectExchange(EXCHANGE_A);
}
/**
* 设置队列
*/
@Bean
public Queue queueA() {
return new Queue(QUEUE_A, true);
}
/**
* 绑定
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(queueA()).to(exchangeA()).with(ROUTINGKEY_A);
}
@Bean
@Scope("prototype")//通知Spring把被注解的Bean变成多例 表示每次获得bean都会生成一个新的对象
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(true);
template.setMessageConverter(new SerializerMessageConverter());
return template;
}
}
yml文件配置
spring:
#RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#必须配置这个才会确认回调
publisher-confirm-type: correlated
#消息投递到队列失败是否回调
publisher-returns: true
创建ConfirmCallbackService.java实现手动ack回执回调处理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
private static final Logger log = LoggerFactory.getLogger(ConfirmCallbackService.class);
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
log.error("消息发送异常!");
} else {
log.info("发送者已经收到确认,correlationData={} ,ack={}, cause={}", correlationData.getId(), ack, cause);
}
}
}
创建ReturnCallbackService.java实现消息投递到队列失败是否回调处理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {
private static final Logger log = LoggerFactory.getLogger(ReturnCallbackService.class);
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("returnedMessage ===> replyCode={} ,replyText={} ,exchange={} ,routingKey={}", replyCode, replyText, exchange, routingKey);
}
}
创建ProducerService.java发送消息处理
import org.springframework.amqp.core.MessageDeliveryMode;
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 java.util.UUID;
@Service
public class ProducerService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ConfirmCallbackService confirmCallbackService;
@Autowired
private ReturnCallbackService returnCallbackService;
/**
* 通用发送消息
*
* @param exchange 交换机
* @param routingKey 路由key
* @param msg 消息
*/
public void sendMessage(String exchange, String routingKey, Object msg) {
/**
* 确保消息发送失败后可以重新返回到队列中
* 注意:yml需要配置 publisher-returns: true
*/
rabbitTemplate.setMandatory(true);
/**
* 消费者确认收到消息后,手动ack回执回调处理
*/
rabbitTemplate.setConfirmCallback(confirmCallbackService);
/**
* 消息投递到队列失败回调处理
*/
rabbitTemplate.setReturnCallback(returnCallbackService);
/**
* 发送消息
*/
rabbitTemplate.convertAndSend(exchange, routingKey, msg,
message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
},
new CorrelationData(UUID.randomUUID().toString()));
}
}
创建RabbitMQController.java类发送消息接口
@RestController
public class RabbitMQController {
private static final Logger log = LoggerFactory.getLogger(RabbitMQController.class);
private static final String SUCCESS = "success";
@Autowired
private ProducerService producerService;
@GetMapping("send")
public String send() {
producerService.sendMessage(DirectRabbitMQConfig.EXCHANGE_A, DirectRabbitMQConfig.ROUTINGKEY_A, "hello 你好!!!");
return SUCCESS;
}
}
启动项目访问http://localhost:8080/send,页面打印success说明发送成功
可以登录http://ip:15672查看
这里我刚才点了三次,所以有三个
下面我们在创建一个springboot项目rabbitmq-consumer进行消息的消费,引入依赖相同
配置yml
server:
port: 8081
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#消费端配置
listener:
simple:
# 同一个队列启动几个消费者
concurrency: 5
# 消费者最大数量
max-concurrency: 10
# 限流 多数据量同时只能过来一条
prefetch: 1
#手动确认
acknowledge-mode: manual
default-requeue-rejected: true
template:
mandatory: true
创建类ReceiverMessage.java 监听消息
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
@Component
public class ReceiverMessage {
private static final Logger log = LoggerFactory.getLogger(ReceiverMessage.class);
@RabbitListener(queues = "queue-a")
public void processHandler1(String msg, Message message, Channel channel) throws Exception {
log.info("消费者A收到消息:{}", msg);
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
//TODO 具体业务
//手动确认消息
channel.basicAck(tag, false);
} catch (Exception e) {
log.error("Exception:" + e.getMessage(), e);
boolean flag = (boolean) headers.get(AmqpHeaders.REDELIVERED);
if (flag) {
log.error("消息已重复处理失败,拒绝再次接收...");
channel.basicAck(tag, false);
} else {
log.error("消息即将再次返回队列处理...");
channel.basicNack(tag, false, true);
}
}
}
}
启动项目后可以看到控制台打印
说明消息消费成功
发送和接受实体类必须保证发送者和接收者bean对象都必须序列化,bean定义必须一模一样,包括bean所在路径
下面以实体类演示一下 延迟队列发送
在两个项目相同的包下面创建Order类并实现Serializable
import java.io.Serializable;
public class Order implements Serializable{
private String orderId; // 订单id
private Integer orderStatus; // 订单状态 0:未支付,1:已支付,2:订单已取消
private String orderName; // 订单名字
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public Integer getOrderStatus() {
return orderStatus;
}
public void setOrderStatus(Integer orderStatus) {
this.orderStatus = orderStatus;
}
public String getOrderName() {
return orderName;
}
public void setOrderName(String orderName) {
this.orderName = orderName;
}
@Override
public String toString() {
return "Order{" +
"orderId='" + orderId + '\'' +
", orderStatus=" + orderStatus +
", orderName='" + orderName + '\'' +
'}';
}
}
创建DelayRabbitConfig.java配置类
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* 延迟队列配置
*/
@Configuration
public class DelayRabbitConfig {
/**
* 延迟队列 TTL 名称
*/
private static final String ORDER_DELAY_QUEUE = "user.order.delay.queue";
/**
* DLX,dead letter发送到的 exchange
* 延时消息就是发送到该交换机的
*/
public static final String ORDER_DELAY_EXCHANGE = "user.order.delay.exchange";
/**
* routing key 名称
* 具体消息发送在该 routingKey 的
*/
public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";
public static final String ORDER_QUEUE_NAME = "user.order.queue";
public static final String ORDER_EXCHANGE_NAME = "user.order.exchange";
public static final String ORDER_ROUTING_KEY = "order";
/**
* 延迟队列配置
*
* 1、params.put("x-message-ttl", 5 * 1000);
* 第一种方式是直接设置 Queue 延迟时间 但如果直接给队列设置过期时间,这种做法不是很灵活,(当然二者是兼容的,默认是时间小的优先)
* 2、rabbitTemplate.convertAndSend(book, message -> {
* message.getMessageProperties().setExpiration(2 * 1000 + "");
* return message;
* });
* 第二种就是每次发送消息动态设置延迟时间,这样我们可以灵活控制
**/
@Bean
public Queue delayOrderQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);
return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
}
/**
* 需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。
* 这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,
* 不会转发dog.puppy,也不会转发dog.guard,只会转发dog。
* @return DirectExchange
*/
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange(ORDER_DELAY_EXCHANGE);
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
}
@Bean
public Queue orderQueue() {
return new Queue(ORDER_QUEUE_NAME, true);
}
/**
* 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。
* 符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。
**/
@Bean
public TopicExchange orderTopicExchange() {
return new TopicExchange(ORDER_EXCHANGE_NAME);
}
@Bean
public Binding orderBinding() {
// TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键
return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
}
}
在ProducerService类中添加一行代码
message.getMessageProperties().setExpiration(1000 * 30 + "");
在RabbitMQController类中 添加接口测试发送消息
@GetMapping("sendDelay")
public String sendDelay() {
Order order = new Order();
order.setOrderId("123456");
order.setOrderName("一加9");
order.setOrderStatus(1);
log.info("【订单生成时间】" + new Date().toString() + "【1分钟后检查订单是否已经支付】" + order.toString());
producerService.sendMessage(DelayRabbitConfig.ORDER_DELAY_EXCHANGE, DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order);
return SUCCESS;
}
在消费者项目rabbitmq-consumer的ReceiverMessage类中中添加队列监听
@RabbitListener(queues = "user.order.queue")
public void processHandler2(Order order, Message message, Channel channel) throws Exception {
log.info("消费者A收到消息:{}", order.toString());
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
//TODO 具体业务
//手动确认消息
channel.basicAck(tag, false);
} catch (Exception e) {
log.error("Exception:" + e.getMessage(), e);
boolean flag = (boolean) headers.get(AmqpHeaders.REDELIVERED);
if (flag) {
log.error("消息已重复处理失败,拒绝再次接收...");
channel.basicAck(tag, false);
} else {
log.error("消息即将再次返回队列处理...");
channel.basicNack(tag, false, true);
}
}
}
重启两个项目访问http://localhost:8080/sendDelay,消费发送成功等待一分钟后查看控制台,打印如下
延迟队列发送成功,延迟队列原博文https://blog.csdn.net/lizc_lizc/article/details/80722763
下面通过整合redis来防止重复消费
在rabbitmq-consumer项目pom.xml文件中加入redis依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
yml文件中加入redis配置,完整配置如下
server:
port: 8081
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#消费端配置
listener:
simple:
# 同一个队列启动几个消费者
concurrency: 5
# 消费者最大数量
max-concurrency: 10
# 限流 多数据量同时只能过来一条
prefetch: 1
#手动确认
acknowledge-mode: manual
# Redis数据库索引(默认为0)
redis:
database: 0
# Redis服务器地址
host: 192.168.0.150
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 链接超时时间 单位 ms(毫秒)
timeout: 3000
添加队列监听,如果有多个方法监听同一个队列,是采用轮询的方式消费的,这里我已经把原来的方法注释掉了
这里消费后添加到redis中,再次消费先查询redis是否存在,这里采用1/0使程序报错然后再次放入到队列消费进行演示
@Autowired
private RedisTemplate redisTemplate;
@RabbitListener(queues = "queue-a")
public void processHandler3(String msg, Message message, Channel channel) throws Exception {
log.info("消费者A收到消息:{}", msg);
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
//TODO 具体业务
String msgId = (String) headers.get("spring_returned_message_correlation");//发送者 需发送一个唯一id
if (redisTemplate.opsForHash().entries("test").containsKey(msgId)) {
//redis 中包含该 key,说明该消息已经被消费过
log.info(msgId + ":消息已经被消费");
channel.basicAck(tag, false);//确认消息已消费
return;
}
//添加到redis
redisTemplate.opsForHash().put("test", msgId, "testDelay");
int i = 1 / 0;//走到这里报错
//手动确认消息
channel.basicAck(tag, false);
} catch (Exception e) {
log.error("Exception:" + e.getMessage());
boolean flag = (boolean) headers.get(AmqpHeaders.REDELIVERED);
if (flag) {
log.error("消息已重复处理失败,拒绝再次接收...");
channel.basicAck(tag, false);
} else {
log.error("消息即将再次返回队列处理...");
channel.basicNack(tag, false, true);
}
}
}
发送消息后可以看到不会在重复消费
如果报错使用basicNack方法重新放回队列,可以查看Message中amqp_redelivered参数变成true(首次为false),所以如果消息消费报错 也不会重复消费,防止死循环
好了,今天就讲到这里!!!
项目已上传https://gitee.com/hehedabiao/springboot-series