目录
1、自动分列延时队列
2、应答失败自动转储延时再通知机制
-------------------------------------------------------------------------------------------------------------------------------
一、自动分列延时队列
根据消息延时时间自动按D(天)、H(时)、M(分)、S(秒)各粒度进行自动建立队列及发送到对应的延时队列中,降低不同时间长度的延时队列做队列再延时的频率。
例如38天的延时消息会自动发送到20天时长队列(D20)中,20天后自动发送到10天时长队列(D10)中,10天后还剩8天就会自动发到1天时长队列中,一天后继续发到1天时长队列中,直到小于1天再按时、分、秒的粒度一次转发到对应的延时队列。
这样的动态粒度控制可以
1、避免粒度太小转发频率变大,队列转发损耗太大,单队列累积数量太多
2、粒度太大,最后的接收处理消息时间节点出现较大误差
当前设置的粒度为D20,D10,D1,H20,H10,H1,M40,M20,M10,S40,S20,S10,S9,S8,S7,S6,S5,S4,S3,S2,S1。可以看到这里10秒以后的延时都是每个一秒有一个对应的时长队列,主要是考虑到如果设置的延时消息是秒级,那么说明消息对延时时间敏感,那么每一次消息交换消耗的时间都是影响较大的,这里如果S10后就设置一个S1的那么如果8秒的就要转换8次,损耗太大,而有了S8那么就直接入S8等待8s后直接消费就行了。
其中的粒度可以根据实际情况自己配置。
代码如下:
1、固定队列配置类
package com.tb.app.configurer.mq.rabbit.producer;
import java.util.HashMap;
import java.util.Map;
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.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 延时消息队列-扩展
* @author tangbin
* @date 2021年6月18日
*/
@Configuration
public class RabbitmqProducerDelayExtendsConfig {
/**
* =========中转消费队列
*/
public final static String DELAY_EXTENDS_XF_TRANS_QUEUE = "delay_extends_xf_trans_queue";
public final static String DELAY_EXTENDS_XF_TRANS_EXCHANGE = "delay_extends_xf_trans_exchange";
public final static String DELAY_EXTENDS_XF_TRANS_ROUTING = "delay_extends_xf_trans_routing";
/**
* =========最终消费队列
*/
public final static String DELAY_EXTENDS_XF_FINAL_QUEUE = "delay_extends_xf_final_queue";
public final static String DELAY_EXTENDS_XF_FINAL_EXCHANGE = "delay_extends_xf_final_exchange";
public final static String DELAY_EXTENDS_XF_FINAL_ROUTING = "delay_extends_xf_final_routing";
/**
* =========死信队列
*/
public final static String DELAY_EXTENDS_DEAD_QUEUE = "delay_extends_dead_queue";
public final static String DELAY_EXTENDS_DEAD_EXCHANGE = "delay_extends_dead_exchange";
public final static String DELAY_EXTENDS_DEAD_ROUTING = "delay_extends_dead_routing";
/**
* =========最终消费队列中应答失败消息转储队列
*/
public final static String DELAY_EXTENDS_XF_FINAL_ACKFAIL_QUEUE = "delay_extends_xf_final_ackfail_queue";
public final static String DELAY_EXTENDS_XF_FINAL_ACKFAIL_ROUTING = "delay_extends_xf_final_ackfail_routing";
public final static String DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE = "delay_extends_xf_final_ackfail_exchange";
/**
* 中转消费队列
* @return
*/
@Bean
public Queue xfTransQueue(){
return new Queue(DELAY_EXTENDS_XF_TRANS_QUEUE);
}
/**
* 中转消费交换机
* @return
*/
@Bean
public DirectExchange xfTransExchange(){
return new DirectExchange(DELAY_EXTENDS_XF_TRANS_EXCHANGE);
}
/**
* 绑定 中转消费队列和交换机
* @return
*/
@Bean
public Binding bindingXfTrans(){
return BindingBuilder.bind(xfTransQueue()).to(xfTransExchange()).with(DELAY_EXTENDS_XF_TRANS_ROUTING);
}
//====================================================================================================================
/**
* 最终消费队列
*
* 如果应答失败转储到 “最终消费应答失败转储队列”
*
* @return
*/
@Bean
public Queue xfFinalQueue(){
Map<String,Object> args = new HashMap<>();
//指定过期消息跳转的交换器名称
args.put("x-dead-letter-exchange",DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE);
//指定跳转消息携带的roting-key
args.put("x-dead-letter-routing-key",DELAY_EXTENDS_XF_FINAL_ACKFAIL_ROUTING);
return QueueBuilder.durable(DELAY_EXTENDS_XF_FINAL_QUEUE).withArguments(args).build();
}
/**
* 最终消费交换机
* @return
*/
@Bean
public DirectExchange xfFinalExchange(){
return new DirectExchange(DELAY_EXTENDS_XF_FINAL_EXCHANGE);
}
/**
* 绑定 最终消费队列和交换机
* @return
*/
@Bean
public Binding bindingXfFinal(){
return BindingBuilder.bind(xfFinalQueue()).to(xfFinalExchange()).with(DELAY_EXTENDS_XF_FINAL_ROUTING);
}
//==============================================================================================================
/**
* 最终消费应答失败转储队列
*
* 超过5分钟后自动再次下发到最终消费队列进行处理
*
* @return
*/
@Bean
public Queue xfFinalACKFailQueue(){
Map<String,Object> args = new HashMap<>();
//指定过期时间
args.put("x-message-ttl", 5*60*1000);
//指定过期消息跳转的交换器名称
args.put("x-dead-letter-exchange",DELAY_EXTENDS_XF_FINAL_EXCHANGE);
//指定跳转消息携带的roting-key
args.put("x-dead-letter-routing-key",DELAY_EXTENDS_XF_FINAL_ROUTING);
return QueueBuilder.durable(DELAY_EXTENDS_XF_FINAL_ACKFAIL_QUEUE).withArguments(args).build();
}
/**
* 最终消费应答失败转储交换机
* @return
*/
@Bean
public DirectExchange xfFinalACKFailExchange(){
return new DirectExchange(DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE);
}
/**
* 绑定 最终消费应答失败转储 队列和交换机
* @return
*/
@Bean
public Binding bindingXfFinalACKFail(){
return BindingBuilder.bind(xfFinalACKFailQueue()).to(xfFinalACKFailExchange()).with(DELAY_EXTENDS_XF_FINAL_ACKFAIL_ROUTING);
}
}
这个类中定义了
1、中转队列(对阶段性延时队列到期做再延时中转的队列)
2、最终消费队列(最终延时结束,即到达设置的消息延时时间后的消息队列)
3、最终消费应答失败转储队列(用于应答失败消息的转储,避免消息丢失及消息堵塞)
4、动态创建死信队列(这个类只是定义了死信队列的共同名字属性,队列的创建在另一个类)
2、动态死信队列消息发送及创建类
package com.tb.app.configurer.mq.rabbit.producer;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.common.collect.Maps;
import com.rabbitmq.client.Channel;
import com.tb.app.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
/**
* 延时消息队列-扩展版
* @author tangbin
* @date 2021年6月18日
*/
@Component
@Slf4j
public class RabbitmqProducerDelayExtendsFactory{
@Autowired
RabbitTemplate rabbitTemplate;
/**
*
* 发送延时消息
*
* @param sendDate 发消息的时间点的时间戳
* @param timeDelay 延时时间,单位毫秒,最大不能超过4294967295(49.710269618055555天)
* @param message 消息内容
* @param sendName 发送者描述
* @throws Exception
*/
public void sendDelayMessage(Long sendDateTimestamp,Long timeDelay,String sendName,JSONObject message){
//计算过时时间节点
sendDateTimestamp = Optional.ofNullable(sendDateTimestamp).orElseGet(()->new Date().getTime());
Long endDateTime = sendDateTimestamp+timeDelay;
//计算当前还剩余的过时时间
Long newTimeDelay = endDateTime - new Date().getTime();
/**
* 消费队列指定
*/
//默认中转消费队列
String DEXE = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_EXCHANGE;
String DEXR = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_ROUTING;
//判断时间
if (newTimeDelay < 10000) {
//时间在10秒以内,指定为最终消费队列
DEXR = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_ROUTING;
DEXE = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_EXCHANGE;
}
/**
* 延时队列指定
*/
Map<String, Object> resMap = queLabel(newTimeDelay);
String label = (String) resMap.get("label");
Long queueDelay = (Long) resMap.get("queueDelay");
String DEDE = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_DEAD_EXCHANGE+label;
String DEDQ = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_DEAD_QUEUE+label;
String DEDR = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_DEAD_ROUTING+label;
//创建连接连接到MabbitMQ
ConnectionFactory factory = rabbitTemplate.getConnectionFactory();
Connection connection =factory.createConnection();
//创建一个频道
Channel channel = connection.createChannel(false);
//创建delay queue
HashMap<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-message-ttl", queueDelay);
arguments.put("x-dead-letter-exchange", DEXE);
arguments.put("x-dead-letter-routing-key", DEXR);
try {
channel.exchangeDeclare(DEDE,"direct",true);
channel.queueDeclare(DEDQ, true, false, false, arguments);
channel.queueBind(DEDQ,DEDE,DEDR);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("队列创建失败!");
}
//往队列中发出一条消息
String sendPath = sendName+"->"+DEDQ;
JSONObject sendDataJsonObject = new JSONObject().element("data",message)
.element("timeDelay", timeDelay)
.element("newTimeDelay", newTimeDelay)
.element("endDateTime", endDateTime)
.element("sendDateTimestamp", sendDateTimestamp)
.element("sendPath", sendPath);
log.info("发送消息 : " + sendDataJsonObject.toString());
rabbitTemplate.convertAndSend(DEDQ, sendDataJsonObject);
//关闭连接
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("通的关闭失败!");
}
connection.close();
}
/**
* 获取队列标签
* @return
*/
private static Map<String, Object> queLabel(Long timeDelay) {
Map<String, Object> resMap = Maps.newHashMap();
//延时队列标签
String label = "S1";
//延时队列延时时间
Long queueDelay = 1000l;
if (timeDelay < 10*1000) {
//10秒以内
for (int i = 9; i >= 0; i--) {
if (timeDelay >= i*1000) {
//找到最小临界值
label = "S"+i;
queueDelay = i*1000l;
break;
}
}
}else if (Math.floor(timeDelay/(1*24*60*60*1000*1.0)) >= 20) {
//大于20天
label = "D"+20;
queueDelay = 24*60*60*1000*20l;
}else if (Math.floor(timeDelay/(1*24*60*60*1000*1.0)) >= 10) {
//大于10天
label = "D"+10;
queueDelay = 24*60*60*1000*10l;
}else if (Math.floor(timeDelay/(1*24*60*60*1000*1.0)) >= 1) {
//大于1天
label = "D"+1;
queueDelay = 24*60*60*1000*1l;
}else if (Math.floor(timeDelay/(60*60*1000*1.0)) >= 20) {
//大于20小时
label = "H"+20;
queueDelay = 60*60*1000*20l;
}else if (Math.floor(timeDelay/(60*60*1000*1.0)) >= 10) {
//大于10小时
label = "H"+10;
queueDelay = 60*60*1000*10l;
}else if (Math.floor(timeDelay/(60*60*1000*1.0)) >= 1) {
//大于1小时
label = "H"+1;
queueDelay = 60*60*1000*1l;
}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 40) {
//大于40分钟
label = "M"+40;
queueDelay = 60*1000*40l;
}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 20) {
//大于20分钟
label = "M"+20;
queueDelay = 60*1000*20l;
}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 10) {
//大于10分钟
label = "M"+10;
queueDelay = 60*1000*10l;
}else if (Math.floor(timeDelay/(60*1000*1.0)) >= 1) {
//大于1分钟
label = "M"+1;
queueDelay = 60*1000*1l;
}else if (Math.floor(timeDelay/(1000*1.0)) >= 40) {
//大于40秒
label = "S"+40;
queueDelay = 1000*40l;
}else if (Math.floor(timeDelay/(1000*1.0)) >= 20) {
//大于20秒
label = "S"+20;
queueDelay = 1000*20l;
}else if (Math.floor(timeDelay/(1000*1.0)) >= 10) {
//大于10秒
label = "S"+10;
queueDelay = 1000*10l;
}
resMap.put("label", label);
resMap.put("queueDelay", queueDelay);
return resMap;
}
}
这个类中定义了
1、时间粒度控制方法queLabel,可以自己定义粒度
2、动态创建延时队列及动态指定转发到的再延时队列
3、消费者中转消息再延时监听类
@Component
@RabbitListener(queues = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE)//监听的队列名称
@Slf4j
public class RabbitmqDelayComsumerExtendsTransComsumer {
@Autowired
RabbitmqProducerDelayExtendsFactory rabbitmqProducerDelayExtendsFactory;
//@RabbitHandler
public void process(JSONObject result) {
log.info("消费者收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE+"消息 : " + result.toString());
Long timeDelay = result.getLong("timeDelay");
Long sendDateTimestamp = result.getLong("sendDateTimestamp");
JSONObject data = result.getJSONObject("data");
//重新发到延时队列中
rabbitmqProducerDelayExtendsFactory.sendDelayMessage(sendDateTimestamp, timeDelay,RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE, data);
}
@RabbitHandler
public void receiveObjectDel(Channel channel, JSONObject json, Message message){
log.info("接收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_TRANS_QUEUE+"的消息:"+json);
//手动ACK
//默认情况下如果一个消息被消费者所正确接收则会被从队列中移除
//如果一个队列没被任何消费者订阅,那么这个队列中的消息会被 Cache(缓存),
//当有消费者订阅时则会立即发送,当消息被消费者正确接收时,就会被从队列中移除
try {
//手动ack应答
//最后一个参数是:是否重回队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
process(json);
//拒绝消息
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
//消息被丢失
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
//消息被重新发送
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
//多条消息被重新发送
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, true);
//log.info("消息消费成功:id:{}",message.getMessageProperties().getDeliveryTag());
} catch (IOException e) {
e.printStackTrace();
log.info("消息消费失败:id:{}",message.getMessageProperties().getDeliveryTag());
}
}
}
这个类是一个消费者类,主要是接收需要再延时的消息进行消息的再延时。
4、最终消费者类
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
import com.tb.app.configurer.mq.rabbit.producer.RabbitmqProducerDelayExtendsConfig;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
@Component
@RabbitListener(queues = RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_QUEUE)//监听的队列名称
@Slf4j
public class RabbitmqDelayComsumerExtendsFinalComsumer {
//@RabbitHandler
public void process(JSONObject result) {
log.info("消费者收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_QUEUE+"消息 : " + result.toString());
}
@RabbitHandler
public void receiveObjectDel(Channel channel, JSONObject json, Message message){
log.info("接收到"+RabbitmqProducerDelayExtendsConfig.DELAY_EXTENDS_XF_FINAL_QUEUE+"的消息:"+json);
//手动ACK
//默认情况下如果一个消息被消费者所正确接收则会被从队列中移除
//如果一个队列没被任何消费者订阅,那么这个队列中的消息会被 Cache(缓存),
//当有消费者订阅时则会立即发送,当消息被消费者正确接收时,就会被从队列中移除
try {
//手动ack应答
//最后一个参数是:是否重回队列
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
//拒绝消息
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
//消息被丢失
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
//消息被重新发送
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
//多条消息被重新发送
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, true);
//log.info("消息消费成功:id:{}",message.getMessageProperties().getDeliveryTag());
} catch (IOException e) {
e.printStackTrace();
log.info("消息消费失败:id:{}",message.getMessageProperties().getDeliveryTag());
}
}
}
这个类也是一个消费者类,作为最后延时完成的消息接收及业务处理,这里设置的拒收消息channel.basicReject进行测试,要注意下
---------------------------------------------------------------------------------------------------------------------------------
二、应答失败自动延时再通知
在固定队列配置类RabbitmqProducerDelayExtendsConfig中的最终消费队列那里声明了死信交换机DELAY_EXTENDS_XF_FINAL_ACKFAIL_EXCHANGE,当应答失败时会自动将消息转发到最终消费应答失败转储队列。
而不是让消息重试,可以让后续正常消息正常处理,解决消息堵塞。转储队列设置的5分钟将消息再转发最终消息队列列尾,即是5分钟后回到正常消费队列排队进行重试。这个时间可以根据具体情况设置。