有时候我们希望需要隔多长时间执行一次,或者消费消息,并不是立刻通知他人,比如30分钟后订单失效,1小时后提醒通知等
这个时候就需要延迟的执行时间,Rabbitmq可以设置队列和消费的过期时间, 我们需要利用两个队列来转发实现这样的延迟消费, queue1队列设置的过期时间,queue2死信队列从queue1队列过期后转发而来,然后消费队列。
话不多说,贴一些重要代码
1 核心 配置类
/**
*
*/
package SpringBootRabbitMQ.demo.mq;
import java.util.HashMap;
import java.util.Map;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ 配置类 ,绑定队列,交换器,路由key
* @author lmc
* 2018年12月19日
*/
@Configuration
public class RabbitmqConfig {
//主函数启动加入依赖,不需要这样配置属性, 配置文件必须以 spring 开头才行
/* @Value("${rabbitmq.host}")
public String host;
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
connectionFactory.setPublisherConfirms(publisherConfirms);// 手动应答模式
return connectionFactory;
}*/
/**
* 方法rabbitAdmin的功能描述:动态声明queue、exchange、routing
* @param connectionFactory
* @return
*/
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory ){
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
//声明死信队列(Fanout类型的exchange)
Queue deadQueue = new Queue(RabbitConstants.QUEUE_NAME_DEAD_QUEUE);
// 死信队列交换机,广播形式
FanoutExchange deadExchange = new FanoutExchange(RabbitConstants.MQ_EXCHANGE_DEAD_QUEUE);
rabbitAdmin.declareQueue(deadQueue);
rabbitAdmin.declareExchange(deadExchange);
rabbitAdmin.declareBinding(BindingBuilder.bind(deadQueue).to(deadExchange));//绑定队列 未指明路由KEY
// Topic 形式 以主题方式发磅,接收者可以用表达比指定所关注的队列,消费消息
// 发放奖励队列交换机 单一模式 只跟队列,路由键,交换器一致才发
DirectExchange exchange = new DirectExchange(RabbitConstants.MQ_EXCHANGE_SEND_AWARD);
//声明发送优惠券的消息队列(Direct类型的exchange) 设置延迟队列的关键
Queue couponQueue = queue(RabbitConstants.QUEUE_NAME_SEND_COUPON);
rabbitAdmin.declareQueue(couponQueue);
rabbitAdmin.declareExchange(exchange);
rabbitAdmin.declareBinding(BindingBuilder.bind(couponQueue).to(exchange).with(RabbitConstants.MQ_ROUTING_KEY_SEND_COUPON));
// 测试不走死信队列或者延迟队列,直接发发送到队列
Queue testQueue = new Queue(RabbitConstants.QUEUE_NAME_SEND_TEST);
rabbitAdmin.declareQueue(testQueue);
rabbitAdmin.declareExchange(exchange);
rabbitAdmin.declareBinding(BindingBuilder.bind(testQueue).to(exchange).with(RabbitConstants.MQ_ROUTING_KEY_SEND_TEST));
return rabbitAdmin;
}
//1 重写了queue方式 ,主要是为了设置死信队列关联,绑定和转发的目的
//2 如果有队列延时不同,必须定制多个队列绑定,因为队列消费是从顶端开始丢弃到死信队列,如果前一条消息过期时间1个小时,第二条30分钟,这样也只会等待第一条1小时的队列丢弃后才丢弃第二条30分钟过期的队列
//所以需要保证同一队列过期时间是一致 PS 可以通过设置消息的expiration字段或者x-message-ttl属性来设置过期时间,两者是一样的效果,都设置了过期时间取两者的最小值
public Queue queue(String name) {
Map<String, Object> args = new HashMap<>();
// 设置死信队列
args.put("x-dead-letter-exchange", RabbitConstants.MQ_EXCHANGE_DEAD_QUEUE);
args.put("x-dead-letter-routing-key", RabbitConstants.MQ_ROUTING_KEY_DEAD_QUEUE);
// 设置消息的过期时间, 单位是毫秒
args.put("x-message-ttl", 10000);
// 是否持久化
boolean durable = true;
// 仅创建者可以使用的私有队列,断开后自动删除
boolean exclusive = false;
// 当所有消费客户端连接断开后,是否自动删除队列
boolean autoDelete = false;
return new Queue(name, durable, exclusive, autoDelete, args);
}
}
2 发送消息的类
/**
*
*/
package SpringBootRabbitMQ.demo.mq;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import com.alibaba.fastjson.JSON;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* Rabbit 发送消息配置类
* @author lmc
* 2018年12月19日
*/
@Service
public class RabbitSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback, InitializingBean {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Rabbit MQ 客户端注入
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 系统配置 注入
*/
@Autowired
private SystemConfig systemConfig;
/*
* * 发送MQ消息
* @param exchangeName 交换机名称
* @param routingKey 路由名称
* @param message 发送消息体
*/
public void sendMessage(String exchangeName,String routingKey,Object message){
Assert.notNull(message, "message 消息体不能为NULL");
Assert.notNull(exchangeName, "exchangeName 不能为NULL");
Assert.notNull(routingKey, "routingKey 不能为NULL");
// 获取CorrelationData对象,这里重写成自己的数据再封装
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString(), message);
correlationData.setExchange(exchangeName);
correlationData.setRoutingKey(routingKey);
correlationData.setMessage(message);
logger.info("发送MQ消息,消息ID:{},消息体:{}, exchangeName:{}, routingKey:{}",
correlationData.getId(), JSON.toJSONString(message), exchangeName, routingKey);
// 发送消息
this.convertAndSend(exchangeName, routingKey, message, correlationData);
}
/**
* 发送消息
*
* @param exchange 交换机名称
* @param routingKey 路由key
* @param message 消息内容
* @param correlationData 消息相关数据(消息ID)
* @throws AmqpException
*/
private void convertAndSend(String exchange, String routingKey, final Object message, CorrelationData correlationData) throws AmqpException {
try {
rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
} catch (Exception e) {
logger.error("MQ消息发送异常,消息ID:{},消息体:{}, exchangeName:{}, routingKey:{}",
correlationData.getId(), JSON.toJSONString(message), exchange, routingKey, e);
// TODO 保存消息到数据库
}
}
/*
* 调用成功之后 ,回调方法 ,设置可用参数值
*/
@Override
public void afterPropertiesSet() throws Exception {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调, 属于发送一个空队列 (错误的网络分区)脑裂
* 在脑裂的情况下会出现这种情况> rabbitmq3.4.2和以后版本修复了此bug。
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.error("MQ消息发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}",
replyCode, replyText, exchange, routingKey, JSON.toJSONString(message.getBody()));
// TODO 保存消息到数据库
}
/**
* 用于实现消息发送到RabbitMQ交换器后接收ack回调。
* 如果消息发送确认失败就进行重试。
*
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(org.springframework.amqp.rabbit.support.CorrelationData correlationData, boolean ack, String cause) {
if (!ack && correlationData instanceof CorrelationData ) {
CorrelationData correlationDataExtends = (CorrelationData) correlationData;
//消息发送失败,就进行重试,重试过后还不能成功就记录到数据库
if (correlationDataExtends.getRetryCount() < systemConfig.getMqRetryCount()) {
logger.info("MQ消息发送失败,消息重发,消息ID:{},重发次数:{},消息体:{}", correlationDataExtends.getId(),
correlationDataExtends.getRetryCount(), JSON.toJSONString(correlationDataExtends.getMessage()));
// 将重试次数加一,重写CorrelationData是为了得到重试次数
correlationDataExtends.setRetryCount(correlationDataExtends.getRetryCount() + 1);
// 重发发消息
this.convertAndSend(correlationDataExtends.getExchange(), correlationDataExtends.getRoutingKey(),
correlationDataExtends.getMessage(), correlationDataExtends);
}else{
//消息重试发送失败,将消息放到数据库等待补发, 重试发送次数超过3次,直接不发了,保存到数据库或者redis里,后续再处理
logger.warn("MQ消息重发失败,消息入库,消息ID:{},消息体:{}", correlationData.getId(),
JSON.toJSONString(correlationDataExtends.getMessage()));
// TODO 保存消息到数据库
}
} else {
logger.info("消息发送成功并处理成功,消息ID:{}", correlationData.getId());
}
}
}
3 消费队列
/**
*
*/
package SpringBootRabbitMQ.demo.consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import SpringBootRabbitMQ.demo.mq.RabbitConstants;
import SpringBootRabbitMQ.demo.mq.SendMessage;
/**
* 1 延迟队列消费 这里是消费消息的类 customer 消费方 每条队列都可能会有死信队列, 实质上是从正常队列过期没消费就转到这里来消费,达到延迟执行,或者定时执行
* 通过 两个队列结合来达到延迟消费
* 2 首先需要创建2个队列。Queue1和Queue2。Queue1是一个消息缓冲队列,在这个队列里面实现消息的过期转发
* 3 如果遇到不同的任务类型需要不同的延时的话,需要为每一种不同延迟时间的消息建立单独的消息队列
* @author lmc
* 2018年12月19日
*/
@Service
public class DeadMessageListener {
private final Logger logger = LoggerFactory.getLogger(DeadMessageListener.class);
@RabbitListener(queues = RabbitConstants.QUEUE_NAME_DEAD_QUEUE)
public void process(SendMessage sendMessage, Channel channel, Message message) throws Exception {
logger.info("[{}]处理延迟队列消息队列接收数据,消息体:{}", RabbitConstants.QUEUE_NAME_SEND_COUPON, JSON.toJSONString(sendMessage));
System.out.println(message.getMessageProperties().getDeliveryTag());
try {
// 参数校验
Assert.notNull(sendMessage, "sendMessage 消息体不能为NULL");
// TODO 处理消息
// 确认消息已经消费成功
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
logger.error("MQ消息处理异常,消息体:{}", message.getMessageProperties().getCorrelationIdString(), JSON.toJSONString(sendMessage), e);
try {
// TODO 保存消息到数据库
// 确认消息已经消费成功
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception dbe) {
logger.error("保存异常MQ消息到数据库异常,放到死性队列,消息体:{}", JSON.toJSONString(sendMessage), dbe);
// 确认消息将消息放到死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
}
主要的代码都在这里, 设置过期时间的队列要与死信队列绑定,才能实现转发, 这点最关键,其它都跟普通的队列没有什么两样, 主要代码都放到我的github 上了,有兴趣的可以去看看