来记录一下笔记吧
rabbitmq要实现延时队列,主要利用消息的过期时间TTL,和死信机制来做,简单来说,我们可以将需要延时发送的消息,设置过期时间,然后把消息发送到某个队列,并且在这个队列上绑定一个死信交换机,这个死信交换机和另一个队列建立绑定关系,这样只需要等待第一个队列的消息过期,然后mq会自动将过期的消息通过绑定的死信交换机,路由到死信交换机绑定的队列,最后监听队列即可
springboot环境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
mq相关配置
spring.rabbitmq.addresses=
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
#必须为true,否则失败,发送者,接受不到回调消息,false 服务器丢失请求
spring.rabbitmq.template.mandatory=true
#消费端手工ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#消费者最小数量
spring.rabbitmq.listener.simple.concurrency=1
#消费者最大数量,控制并发,若需要消息有消费,则可配置1个消费端
spring.rabbitmq.listener.simple.max-concurrency=1
#每次请求处理的消息数
spring.rabbitmq.listener.simple.prefetch=1
配置:RabbitAdmin ,rabbitAdmin用来操作队列,交换机,
比如说创建队列,创建交换机,建立绑定关系
package com.fnm.feynman.hospital.common.config;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
/**
* @author yanjun.liu
* @version 1.0
* @date 2020/9/2--14:38
*/
@SpringBootConfiguration
public class RabbitMqConfig {
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
rabbitAdmin.setAutoStartup(true);
return rabbitAdmin;
}
}
消息的发送者
解释一下以下代码,最上方,是两个回调,在消息发送成功或者失败,将保存数据库做留存,以备后期对发送失败的消息进行补偿,
下面贴上代码
先发送一条及时消息,然后在发送一条延时消息
package com.fnm.feynman.hospital.business.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fnm.feynman.common.utils.UUIDUtils;
import com.fnm.feynman.hospital.business.entity.ZcMqMessageEntity;
import com.fnm.feynman.hospital.business.resp.OrderMessageResp;
import com.fnm.feynman.hospital.business.service.SendMessageService;
import com.fnm.feynman.hospital.business.service.ZcMqMessageService;
import com.fnm.feynman.hospital.constant.FinalConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.sql.Time;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author yanjun.liu
* @version 1.0
* @date 2020/9/2--12:00
*/
@Service
@Slf4j
public class SendMessageServiceImpl implements SendMessageService {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private RabbitAdmin rabbitAdmin;
@Resource
private ZcMqMessageService zcMqMessageService;
private static final String ROUTE_KING="#";
private static final String DELAYED="_delayed";
private static final String TTL="_ttl";
/**
* 回调函数: confirm确认
* correlationData 消息唯一id
* ack 是否到达Broker
* cause nack出现异常,返回的异常消息
*/
final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
log.error("消息发送到Broker失败,消息id为:{}",correlationData);
updateMessageStatus(FinalConst.ERROR,correlationData,FinalConst.CONFIRM);
} else {
updateMessageStatus(FinalConst.SUCCESS,correlationData,FinalConst.CONFIRM);
log.info("消息发送到Broker成功,消息id为:{}",correlationData);
}
}
};
private void updateMessageStatus(String status,CorrelationData correlationData,String callbackType){
ZcMqMessageEntity zcMqMessageEntity=zcMqMessageService.getOne(new QueryWrapper<ZcMqMessageEntity>().lambda().eq(ZcMqMessageEntity::getMqMessageId,correlationData.getId()));
if(zcMqMessageEntity != null){
zcMqMessageEntity.setIsSend(FinalConst.SUCCESS);
zcMqMessageEntity.setCallbackType(callbackType);
zcMqMessageService.updateById(zcMqMessageEntity);
}
}
/**
* return返回回调
* 若routingKey不可达。会回调此方法
* message 消息
* replyCode 不可达错误码
* replyText 错误内容信息
* exchange 交换机
* routingKey 路由key
*/
final RabbitTemplate.ReturnCallback returnCallback = new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(org.springframework.amqp.core.Message message, int replyCode, String replyText,
String exchange, String routingKey) {
String messageId = message.getMessageProperties().getMessageId();
ZcMqMessageEntity zcMqMessageEntity=zcMqMessageService.getOne(new QueryWrapper<ZcMqMessageEntity>().lambda().eq(ZcMqMessageEntity::getMqMessageId,messageId));
if(zcMqMessageEntity != null){
zcMqMessageEntity.setIsSend(FinalConst.ERROR);
zcMqMessageEntity.setCallbackType(FinalConst.RETURN);
zcMqMessageService.updateById(zcMqMessageEntity);
}
log.error("消息发送失败:routingKey不可达,消息为:{}",message.getMessageProperties().getMessageId());
log.error("消息发送失败:routingKey不可达,消息为:{}",message);
log.error("消息发送失败:routingKey不可达,错误码:{}",replyCode);
log.error("消息发送失败:routingKey不可达,错误内容信息:{}",replyText);
log.error("消息发送失败:routingKey不可达,交换机:{}",exchange);
log.error("消息发送失败:routingKey不可达,路由key:{}",routingKey);
}
};
/**
* 发送预约消息
* 若预约时间减去一小时小于,<当前时间,那么消息会丢失,延时队列不会收到该消息
* @param queue
* @param orderMessageResp
*/
@Override
public void sendOrderMessage(String exchange, String queue, OrderMessageResp orderMessageResp, Date date, Time time,String orderNo){
String json = JSON.toJSONString(orderMessageResp);
rabbitAdmin.declareExchange(new TopicExchange(exchange, true, false));
rabbitAdmin.declareQueue(new Queue(queue, true));
rabbitAdmin.declareBinding(
BindingBuilder
.bind(new Queue(queue))
.to(new TopicExchange(exchange))
.with(ROUTE_KING));
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setReturnCallback(returnCallback);
//消息id = 时间戳 + 全局唯一id
CorrelationData correlationData = new CorrelationData(UUIDUtils.generate32BitId() + Long.toString(System.currentTimeMillis()));
orderMessageResp.setTitle("预约成功");
//入库
saveMessage(json,exchange,queue,orderNo,correlationData.getId(),ROUTE_KING,FinalConst.NOT_DELAY);
//发送
rabbitTemplate.convertAndSend(exchange, ROUTE_KING, json, correlationData);
log.info("消息发送成功,消息内容为:{}",json);
//开始发送延时消息,上面一部分是发送的即时消息
//声明死信交换机,绑定到队列
Map<String, Object> agruments = new HashMap<>(2);
agruments.put("x-dead-letter-exchange", exchange+TTL);
agruments.put("x-dead-letter-routing-key", ROUTE_KING);
rabbitAdmin.declareQueue(new Queue(queue+DELAYED, true,false, false,agruments));
rabbitAdmin.declareExchange(new TopicExchange(exchange+DELAYED, true, false));
rabbitAdmin.declareBinding(
BindingBuilder
.bind(new Queue(queue+DELAYED))
.to(new TopicExchange(exchange+DELAYED))
.with(ROUTE_KING));
orderMessageResp.setMessage(orderMessageResp.getMessage()+",距离预约时间还有一小时,请您及时前往");
LocalDateTime localDateTime = time.toLocalTime().atDate(LocalDate.from(date.toInstant().atZone(ZoneId.systemDefault()))).minusHours(1);
//Long duration = null;
long duration = java.time.Duration.between(LocalDateTime.now(), localDateTime).toMillis();
if(duration<0){
duration= 30000L;
orderMessageResp.setMessage(orderMessageResp.getMessage()+",您的预约时间不足一小时,请您及时前往");
}
orderMessageResp.setTitle("预约提醒");
String delayedMessage = JSON.toJSONString(orderMessageResp);
log.info("{}:毫秒之后过期,到死信队列",duration);
//发送消息到延时队列,等待过期
long finalDuration = duration;
//延时队列消息id
CorrelationData delayedCorrelationData = new CorrelationData(UUIDUtils.generate32BitId() + Long.toString(System.currentTimeMillis()));
//入库
saveMessage(delayedMessage,exchange+DELAYED,queue+DELAYED,orderNo,delayedCorrelationData.getId(),ROUTE_KING,FinalConst.YES_DELAY);
rabbitTemplate.convertAndSend(exchange+DELAYED, ROUTE_KING, delayedMessage, message -> {
MessageProperties messageProperties = message.getMessageProperties();
// 设置这条消息的过期时间
messageProperties.setExpiration(Long.valueOf(finalDuration).toString());
return message;
}, delayedCorrelationData);
log.info("发送消息到延迟队列成功,消息为{},消息id为:{}",delayedMessage,correlationData);
//死信交换机绑定死信队列
rabbitAdmin.declareQueue(new Queue(queue+TTL, true));
rabbitAdmin.declareExchange(new TopicExchange(exchange+TTL, true, false));
rabbitAdmin.declareBinding(
BindingBuilder
.bind(new Queue(queue+TTL))
.to(new TopicExchange(exchange+TTL))
.with(ROUTE_KING));
}
private void saveMessage(String json,String exchange,String queue,String orderNo,String correlationData,String routeKey,String delay){
ZcMqMessageEntity zcMqMessageEntity = new ZcMqMessageEntity();
zcMqMessageEntity.setMessage(json);
zcMqMessageEntity.setExchange(exchange);
zcMqMessageEntity.setQueue(queue);
zcMqMessageEntity.setOrderNumber(orderNo);
zcMqMessageEntity.setMqMessageId(correlationData);
zcMqMessageEntity.setRouteKey(routeKey);
zcMqMessageEntity.setSendTime(new Date());
zcMqMessageEntity.setIsDelay(delay);
zcMqMessageService.save(zcMqMessageEntity);
}
}
以上是消息的发送者,消费者只要监听队列即可,然后做好手动ack确认消息
,因为消费者由前端监听队列消费,所以消费者的部分就省了。。。