此技术目前还有的缺陷说明
举个例子:满足场景1不是问题,满足场景2还需要进一步去优化
场景1:订单十分钟未支付系统取消订单,那么如果用定时器需要几分钟执行一次?一分钟执行一次有误差,那一秒钟执行一次对数据库损耗过大!那么用延时队列可以完美解决问题。
场景2:有两个订单业务,A订单业务30分钟后未支付系统取消支付,B订单业务15分钟后未支付系统取消支付,那么如果同一时间节点下->先下了一个A订单,再下了一个B订单。那么肯定是B订单先被系统取消,然后再是A订单被系统取消,这是正常业务需求。注:这会这个延时队列就有问题了,A订单的这条延时消息不执行,那么B订单这个消息就执行不了。
目前给出笨的解决办法:两个延时队列去执行。(大家看看找找有没有什么办法解决这个问题!猜想可能是因为RabbitMQ严格执行了先进先出的规则,打破这个规则是否可行还未尝试。)
文末已附上解决方案
码云地址
导入POM依赖(需要用到Hutool的布隆过滤器)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置YML,依然采用手动确认消息的方式
server:
port: 8081
spring:
rabbitmq:
# virtual-host: oa-one 这个是指定虚拟主机 没有创建就不需要指定了 如果有需要就在rabbitmq后台管理创建一个
host: 192.168.10.14
port: 5672
username: guest
password: guest
listener:
simple:
# 自动签收,这个是默认行为
# acknowledge-mode: auto
# 手动签收
acknowledge-mode: manual
direct:
# 设置直连交换机的签收类型
acknowledge-mode: manual
# 消息由交换机到达队列失败时触发
publisher-returns: true
# 开启消息到达交换机的确认机制
publisher-confirm-type: correlated
# redis配置 服务器部署请开启密码
redis:
# 地址
host: 192.168.10.14
# 端口,默认为6379
port: 6379
# 密码
password: rick
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
#
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
先搞一个发送消息服务(RabbitSendService),统一走这个发送消息
package com.jen.service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @ClassName RabbitSendService
* @Description 队列发送消息服务
* @Date 2021/11/2 11:39
* @Author Rick Jen
*/
@Component
@Slf4j
public class RabbitSendService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @Description 发送rabbitmq消息
* @author Rick Jen
* @Date 2021/10/19 17:02
* exchange 交换
* routingKey 路由key
* msg 消息 如果要传对象的话一般转为json字符串发送
*/
public void sendMessage(String exchange,String routingKey,String msg){
// 发送MQ消息
rabbitTemplate.convertAndSend(exchange, routingKey, msg,
message -> {
// 生成消息唯一号
String messageId = IdUtil.getSnowflake(1,1).nextIdStr();
// 自己给消息设置自定义的ID
message.getMessageProperties().setMessageId(messageId);
return message;
});
}
/**
* @Description 发送延时消息
* @author Rick Jen
* @Date 2021/11/2 17:21
* @Param queue 延时队列名
* @Param expiration 存活时间 rabbitmq以最小时间为存活时间 单位毫秒
* @Param msg 消息 如果要传对象的话一般转为json字符串发送
*/
public void sendDelayMessage(String queue,Long expiration,String msg){
log.info("发送延时消息:{{}},发送时间:{{}}",msg, DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
// 发送MQ延时消息
rabbitTemplate.convertAndSend(queue, msg, message -> {
// 生成消息唯一号
String messageId = IdUtil.getSnowflake(1,1).nextIdStr();
// 自己给消息设置自定义的ID
message.getMessageProperties().setMessageId(messageId);
// 我们延时队列里面设置的为 (最大毫秒数) 秒,但这个消息它只想活 指定秒 毫秒数 秒
message.getMessageProperties().setExpiration(expiration.toString());
return message;
});
}
}
创建一个类WatchMessageImpl并实现RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback来做消息监控和到达队列的监控
package com.jen.config;
import com.jen.service.RabbitSendService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
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.Component;
import javax.annotation.PostConstruct;
/**
* @ClassName WatchMessageImpl
* @Description 消息监控和到达队列的监控
* @Date 2021/10/19 16:55
* @Author Rick Jen
*/
@Component
@Slf4j
public class WatchMessageImpl implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitSendService rabbitSendService;
@PostConstruct
private void initRabbitTemplate() {
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
}
/**
* 消息到达交换机的回调
*
* @param correlationData
* @param ack 是否到达交换机
* @param cause 如果没有接收成功,返回拒绝的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("交换机接收消息成功");
} else {
log.error("交换机接收消息失败,失败原因为:{}", cause);
}
}
/**
* 当消息到达队列失败时,回调的方法,消息被退回了,我们可以把消息记录下来,分析错误的原因,以后重新发送,这样的话,消息就不会再丢失了
*
* @param message 消息体
* @param replyCode 回退的响应码
* @param replyText 响应文本
* @param exchange 该消息来自哪个交换机
* @param routingKey 该消息的路由key
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("到达队列失败,消息内容:{}", new String(message.getBody()));
log.error("回退的响应码:{},响应文本:{},交换机为:{},路由key为:{}", replyCode, replyText, exchange, routingKey);
// 处理重新发送的问题
// 重新发送消息
log.error("重新发送消息:{}", new String(message.getBody()));
rabbitSendService.sendMessage(exchange, routingKey, new String(message.getBody()));
}
}
初始化延时队列配置DelayedMessageQueueConfig
package com.jen.queue;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName DelayedMessageQueueConfig
* @Description 延时消息队列配置
* @Date 2021/12/17 11:16
* @Author Rick Jen
*/
@Configuration
@Slf4j
public class DelayedMessageQueueConfig {
/**
* 逾期延时队列
*/
private final static String DELAYED_QUEUE = "delayed_queue";
/**
* 新生队列
*/
private final static String NEWLY_BORN_QUEUE = "newly_born_queue";
/**
* 延时交换机
*/
public final static String DELAYED_EXCHANGE = "delayed_exchange";
/**
* 延时路由key
*/
public final static String DELAYED_ROUTING_KEY = "delayed_routing_key";
/**
* rabbitmq延时消息最大延时 毫秒
*/
public final static Long MAX_EXPIRE = 4294967295L;
/**
* 创建延时队列
*/
@Bean
public Queue leaderAutoFinishTaskDelayQueue() {
log.info("创建延时队列:{}", DELAYED_QUEUE);
Map<String, Object> map = new HashMap<>();
// 把一个队列修改为延迟队列
// 消息的最大存活时间 单位为毫秒(最大延时4294967295毫秒) 一天86400秒
map.put("x-message-ttl", MAX_EXPIRE);
// 该队列里面的消息死了,去哪个交换机
map.put("x-dead-letter-exchange", DELAYED_EXCHANGE);
// 该队列里面的消息死了,去那个交换机,由哪个路由 key 来路由他
map.put("x-dead-letter-routing-key", DELAYED_ROUTING_KEY);
return new Queue(DELAYED_QUEUE, true, false, false, map);
}
/**
* 延时交换机
*/
@Bean
public DirectExchange delayedExchange() {
log.info("创建延时交换机:{}", DELAYED_EXCHANGE);
return new DirectExchange(DELAYED_EXCHANGE);
}
/**
* 新生队列
*/
@Bean
public Queue newlyBornQueue() {
log.info("创建新生队列:{}", NEWLY_BORN_QUEUE);
return new Queue(NEWLY_BORN_QUEUE);
}
/**
* 绑定
*/
@Bean
public Binding bindingQueue() {
log.info("通过路由Key:{},将队列:{},绑定到延时交换机:{}上", DELAYED_ROUTING_KEY, NEWLY_BORN_QUEUE, DELAYED_EXCHANGE);
return BindingBuilder.bind(newlyBornQueue()).to(delayedExchange()).with(DELAYED_ROUTING_KEY);
}
}
实例化布隆过滤器BloomFilterConfig 需要配置Redis
package com.jen.config;
import cn.hutool.bloomfilter.BitMapBloomFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName BloomFilterConfig
* @Description 布隆过滤拦截器
* @Date 2021/10/19 16:24
* @Author Rick Jen
*/
@Configuration
public class BloomFilterConfig {
@Bean
public BitMapBloomFilter bitMapBloomFilter() {
return new BitMapBloomFilter(1024);
}
}
监听DelayedRabbitListener
package com.jen.listener;
import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.core.date.DateUtil;
import com.jen.queue.DelayedMessageQueueConfig;
import com.jen.service.RabbitSendService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
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.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
/**
* @ClassName DelayedRabbitListener
* @Description RabbitMQ监听新生队列消息
* @Date 2021/12/17 11:46
* @Author Rick Jen
*/
@Component
@RabbitListener(queues = DelayedMessageQueueConfig.NEWLY_BORN_QUEUE)
@Slf4j
public class DelayedRabbitListener {
/**
* 消息的前缀
*/
private final String MESSAGE = "delayed-message:";
@RabbitHandler
@Transactional(rollbackFor = Exception.class)
public void process(String msg, Message message, Channel channel) throws IOException {
log.info("接收时间:{},【DelayedRabbitListener-接收到消息】=> {}", DateUtil.now(), msg);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String messageId = message.getMessageProperties().getMessageId();
log.info("消息投递ID → :{}", deliveryTag);
log.info("消息自定义ID → :{}", messageId);
if (this.bitMapBloomFilter.contains(messageId)) {
log.error("message:{},该消息被消费过,不能重复进行消费", msg);
try {
// 如果进入到这里面,说明这个消息之前被消费过,但是 MQ 认为你没有消费,所以我们要签收这条消息
channel.basicAck(deliveryTag, false);
return;
} catch (Exception e) {
log.error("签收消息异常!异常原因:{}", e.toString());
}
}
// 消息被消费次数统计
String count = this.redisTemplate.opsForValue().get(MESSAGE + messageId);
if (count != null && Long.parseLong(count) >= 3) {
channel.basicNack(deliveryTag, false, false);
// 失败次数过多,这里可能需要人工介入
log.error("该消息:{} ->消费【3】次都失败了!", message);
}
// 签收消息 最后再签收消息
channel.basicAck(deliveryTag, true);
log.info("消息签收成功");
// 消费成功之后放到布隆过滤器里面
this.bitMapBloomFilter.add(messageId);
log.info("消息已被签收,消息内容:{}", msg);
}
@Autowired
private BitMapBloomFilter bitMapBloomFilter;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* rabbit发送服务
*/
@Autowired
private RabbitSendService rabbitSendService;
}
封装发送的消息体DelayedMessageDTO
package com.jen.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @ClassName DelayedMessageDTO
* @Description 延时消息测试对象封装
* @Date 2021/12/17 11:51
* @Author Rick Jen
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DelayedMessageDTO implements Serializable {
/** 消息模块(可以按照模块划分不同类型的业务). */
private String moduleName;
/** 消息内容. */
private String messageContent;
}
测试发送消息TestRabbitMQController
package com.jen.controller;
import com.google.gson.Gson;
import com.jen.dto.DelayedMessageDTO;
import com.jen.queue.DelayedMessageQueueConfig;
import com.jen.service.RabbitSendService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName TestRabbitMQController
* @Description rabbitmq 测试
* @Date 2021/12/17 11:50
* @Author Rick Jen
*/
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/test")
public class TestRabbitMQController {
private final RabbitSendService rabbitSendService;
/**
* @Description 测试延时队列
* @author Rick Jen
* @Date 2021/12/17 14:02
* @Param dto 消息体数据
* @Param delayTime 过期时间
*/
@RequestMapping(value = "/sendDelayedMessage")
public String sendDelayedMessage(DelayedMessageDTO dto, Long delayTime) {
rabbitSendService.sendDelayMessage(DelayedMessageQueueConfig.DELAYED_QUEUE, delayTime, new Gson().toJson(dto));
return "发送成功!";
}
}
调用并观察打印日志
场景1 正常执行
/pic:mw://e2fac6a074be9ca9836cbcc262f84471
场景2 还有问题
/pic:mw://428b5a0e593412194d80128f9e609b09
使用CustomExchange类型的交换机解决场景2出现的问题
去掉WatchMessageImpl消息监控和到达队列的监控
初始化延时队列配置DelayedMessageQueueOtherConfig
package com.jen.queue;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName DelayedMessageQueueOtherConfig
* @Description 延时队列配置(另一种方式使用)
* @Date 2021/12/17 15:38
* @Author Rick Jen
*/
@Configuration
@Slf4j
public class DelayedMessageQueueOtherConfig {
/**
* 逾期延时队列
*/
public final static String DELAYED_OTHER_QUEUE = "delayed_other_queue";
/**
* 延时交换机
*/
public final static String DELAYED_OTHER_EXCHANGE = "delayed_other_exchange";
/**
* 延时路由key
*/
public final static String DELAYED_OTHER_ROUTING_KEY = "delayed_other_routing_key";
/**
* 延时队列交换机
* 注意这里的交换机类型:CustomExchange
*/
@Bean
public CustomExchange delayOtherExchange(){
log.info("other延时队列交换机创建:{}",DELAYED_OTHER_EXCHANGE);
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_OTHER_EXCHANGE,"x-delayed-message",true, false,args);
}
/**
* 延时队列
*/
@Bean
public Queue delayOtherQueue(){
log.info("other延时队列创建:{}",DELAYED_OTHER_QUEUE);
return new Queue(DELAYED_OTHER_QUEUE,true);
}
/**
* 给延时队列绑定交换机
*/
@Bean
public Binding otherBindingQueue(){
return BindingBuilder.bind(delayOtherQueue()).to(delayOtherExchange()).with(DELAYED_OTHER_ROUTING_KEY).noargs();
}
}
在RabbitSendService中定义新的消息发送方法
/**
* @Description 发送延时消息(另一种方法)
* @author Rick Jen
* @Date 2021/12/17 15:53
* @Param exchange 交换机名字
* @Param routingKey 路由key
* @Param expiration 延时毫秒
* @Param msg 消息
*/
public void sendOtherDelayMessage(String exchange,String routingKey,Long expiration,String msg){
log.info("发送延时消息:{},发送时间:{},延时消费毫秒数:{}",msg, DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"),expiration);
// 发送MQ延时消息
rabbitTemplate.convertAndSend(exchange,routingKey, msg, message -> {
// 生成消息唯一号
String messageId = IdUtil.getSnowflake(1,1).nextIdStr();
// 自己给消息设置自定义的ID
message.getMessageProperties().setMessageId(messageId);
// 延时时间
message.getMessageProperties().setDelay(Math.toIntExact(expiration));
return message;
});
}
测试发送
private final RabbitSendService rabbitSendService;
/**
* @Description 测试延时队列
* @author Rick Jen
* @Date 2021/12/17 15:48
* @Param dto 消息体数据
* @Param delayTime 过期时间 单位毫秒
*/
@RequestMapping(value = "/sendOtherDelayedMessage")
public String sendOtherDelayedMessage(DelayedMessageDTO dto, Long delayTime) {
rabbitSendService.sendOtherDelayMessage(DelayedMessageQueueOtherConfig.DELAYED_OTHER_EXCHANGE,DelayedMessageQueueOtherConfig.DELAYED_OTHER_ROUTING_KEY, delayTime, new Gson().toJson(dto));
return "发送成功!";
}