需求与思路:
避免消费端消费消息失败之后重试再消费失败再重试的死循环,将重试计数放入Redis中,超过指定次数后消息进入死信队列。同时关于手动确认这段代码使用AOP做统一处理。
据查阅资料:
开启手动确认后spring的消息重试就不起作用了,加入请求头中计数经我测试也不起作用(或许是我姿势有问题,网上有人介绍这种方法,反正我没成功)故此我选择Redis作为消息重试计数
首先创建队列与交换机,这里采用配置文件方式,不采用注解方式(微服务项目统一管理)
import com.fanlyun.rabbitmq.core.constants.AmqpConstant;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitmqConfig {
// 声明业务Exchange
@Bean("networkLetterExchange")
public TopicExchange networkLetterExchange(){
return new TopicExchange(AmqpConstant.NETWORK_LETTER_EXCHANGE);
}
// 声明死信Exchange
@Bean("deadLetterExchange")
public DirectExchange deadLetterExchange(){
return new DirectExchange(AmqpConstant.DEAD_LETTER_EXCHANGE);
}
// 声明网络运单队列
@Bean("networkQueueReceive")
public Queue networkQueueReceive(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",AmqpConstant.DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", AmqpConstant.DEAD_LETTER_QUEUE_ROUTING_KEY);
// args.put("x-message-ttl",10000);//设置队列消息的超时时间,单位毫秒,超过时间进入死信队列
// args.put("x-max-length", 10);//生命队列的最大长度,超过长度的消息进入死信队列
return QueueBuilder.durable(AmqpConstant.NETWORK_LETTER_QUEUE_RECEIVE).withArguments(args).build();
}
// 声明网络运单队列
@Bean("networkQueueTest")
public Queue networkQueueTest(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",AmqpConstant.DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", AmqpConstant.DEAD_LETTER_QUEUE_ROUTING_KEY);
// args.put("x-message-ttl",10000);//设置队列消息的超时时间,单位毫秒,超过时间进入死信队列
// args.put("x-max-length", 10);//生命队列的最大长度,超过长度的消息进入死信队列
return QueueBuilder.durable(AmqpConstant.NETWORK_LETTER_QUEUE_TEST).withArguments(args).build();
}
// 声明死信队列
@Bean("deadLetterQueue")
public Queue deadLetterQueue(){
return new Queue(AmqpConstant.DEAD_LETTER_QUEUE_NAME);
}
/*
将网络运单队列绑定网络运单交换机 接收网络运单
*/
@Bean
public Binding bindingExchangeNetworkReceive(@Qualifier("networkQueueReceive") Queue queue,@Qualifier("networkLetterExchange") TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(AmqpConstant.NETWORK_LETTER_ROUTING_RECEIVE);
}
/*
将网络运单队列绑定网络运单交换机 测试网络运单
*/
@Bean
public Binding bindingExchangeNetwork(@Qualifier("networkQueueTest") Queue queue,@Qualifier("networkLetterExchange") TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(AmqpConstant.NETWORK_LETTER_ROUTING_TEST);
}
// 声明死信队列绑定关系
@Bean
public Binding deadLetterBinding(@Qualifier("deadLetterQueue") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(AmqpConstant.DEAD_LETTER_QUEUE_ROUTING_KEY);
}
}
队列名称与交换机名称写成常量
/**
* RabbitMq中的常量类 队列名 路由键
*/
public class AmqpConstant {
//死信交换机
public static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";
//死信路由key
public static final String DEAD_LETTER_QUEUE_ROUTING_KEY = "deadletter.queue.routingkey";
//死信队列
public static final String DEAD_LETTER_QUEUE_NAME = "dead_letter_queue_name";
//网络运单交换机
public static final String NETWORK_LETTER_EXCHANGE = "network_letter_exchange";
//网络运单队列名称
public static final String NETWORK_LETTER_QUEUE_TEST = "network_letter_queue_test";
//网络运单队列名称
public static final String NETWORK_LETTER_QUEUE_RECEIVE = "network_letter_queue_receive";
//网络运单队列路由键 测试
public static final String NETWORK_LETTER_ROUTING_RECEIVE = "network.letter.routing.receive";
//网络运单队列路由键 接收
public static final String NETWORK_LETTER_ROUTING_TEST = "network.letter.routing.test";
}
发送消息类
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import java.nio.charset.Charset;
import java.util.UUID;
@Component
public class MessageSender {
@Autowired
private RabbitTemplate rabbitTemplate;
/***
* 发送消息到rabbitMq
* @param exchange 交换机
* @param routingKey 路由键
* @param obj 实体类
*/
public void sendMsg(String exchange,String routingKey,Object obj){
//待转换的实体类
String msg = JSON.toJSONString(obj);
//创建消费对象,并指定全局唯一ID(这里使用UUID,也可以根据业务规则生成,只要保证全局唯一即可)
MessageProperties messageProperties = new MessageProperties();
messageProperties.setMessageId(UUID.randomUUID().toString());
messageProperties.setContentType("text/plain");
messageProperties.setContentEncoding("utf-8");
Message message = new Message(msg.getBytes(Charset.defaultCharset()), messageProperties);
rabbitTemplate.convertSendAndReceive(exchange, routingKey, message);
}
}
自定义消息异常织入点注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageException {
/**
* 消息id存放在Redis时间
*/
public long second() default 0;
/**
* 重试次数
* @return
*/
public int retry() default 0;
/**
* 重试间隔休眠毫秒数
* @return
*/
public long milli() default 0;
}
Aop统一处理消费异常与手动确认
import com.fanlyun.common.core.text.Convert;
import com.fanlyun.common.redis.utils.RedisUtils;
import com.fanlyun.common.core.annotation.MessageException;
import com.fanlyun.rabbitmq.core.enums.ActionEnum;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class ExceptionAspect {
private static final Logger log = LoggerFactory.getLogger(ExceptionAspect.class);
/**
* Pointcut 切入点
* 匹配 @MessageException 有此注解的地方
*/
@Pointcut("@annotation(com.fanlyun.common.core.annotation.MessageException)")
public void safetyAspect() {}
/**
* 环绕通知,在方法执行前后执行
*/
@Around(value = "safetyAspect()")
public Object around(ProceedingJoinPoint pjp) {
log.info("=====================进入监听消息的环绕通知拦截中=======================");
try {
Object param =null;
//method方法
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
//method方法上面的注解
Annotation[] annotations = method.getAnnotations();
//方法的形参参数
Object[] args = pjp.getArgs();
//是否有@MessageException
boolean IsMessageException = false;
for (Annotation annotation : annotations) {
if (annotation.annotationType() == MessageException.class) {
IsMessageException = true;
}
}
Message message=null;
Channel channel=null;
if(IsMessageException){
message =(Message) args[0];
channel=(Channel)args[1];
}else {
param = pjp.proceed(args);
return param;
}
//执行并替换最新形参参数 PS:这里有一个需要注意的地方,method方法必须是要public修饰的才能设置值,private的设置不了
ActionEnum action= ActionEnum.ACCEPT;
String messageId ="";
//消息id存放在Redis时间
long second =0;
//重试间隔休眠毫秒数
long milli=0;
//重试次数
int retry=0;
Long tag =null;
try{
//获取注解上的参数
MessageException annotation = method.getAnnotation(MessageException.class);
retry = annotation.retry();
second = annotation.second();
milli = annotation.milli();
//转换方法上的参数
tag=message.getMessageProperties().getDeliveryTag();
messageId = message.getMessageProperties().getMessageId();
param = pjp.proceed(args);
}catch (Exception e){
// 根据异常种类决定是ACCEPT、RETRY还是 REJECT
action = ActionEnum.RETRY;
e.printStackTrace();
}
finally {
try {
// 通过finally块来保证Ack/Nack会且只会执行一次
if (action == ActionEnum.ACCEPT) {
channel.basicAck(tag,false);
} else if (action == ActionEnum.RETRY) {
boolean b = RetryRedis(messageId, second, retry);
if(!b){
//进入死信队列
channel.basicNack(tag, false, false);
RedisUtils.del(messageId);
}else {
// 重试
channel.basicNack(tag, false, true);
}
Thread.sleep(milli);
// 拒绝消息也相当于主动删除mq队列的消息
} else {
channel.basicNack(tag, false, false);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//返回
return param;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
/***
* 重试次数
* @param messId 消息id
* @param second 消息在Redis中存在秒数
* @param retry 重试次数
* @return
*/
public boolean RetryRedis(String messId,long second,int retry){
Object o = RedisUtils.get(messId);
if (o==null) {
//第一次存入这个消息id key item value
RedisUtils.set(messId,1,second);
}else {
long count = Convert.toLong(RedisUtils.get(messId));
if(count>=retry){
//大于设置的次数
return false;
}else {
RedisUtils.incr(messId,1l);
}
}
return true;
}
}
发送消息测试类
@Autowired
private MessageSender messageSender;
int i = 0;
@PostMapping("/pushTest")
public String testPush(@RequestBody Map<String, Object> map) {
ConsignmentInfo consignmentInfo = new ConsignmentInfo();
TmsConsignment tmsConsignment = new TmsConsignment();
tmsConsignment.setReceiverUserName("胡半仙");
tmsConsignment.setShipperRemark("fsdfsdsdfsdfsdfdsfds");
tmsConsignment.setArriveSite("到达站点");
tmsConsignment.setChangeState("发送到九分裤");
tmsConsignment.setConsignmentFlag("fdsf");
List<TmsConsignmentMaterial> tmsConsignmentMaterialList = new ArrayList<>();
TmsConsignmentMaterial tmsConsignmentMaterial = new TmsConsignmentMaterial();
tmsConsignmentMaterial.setActualVolume(new BigDecimal("90"));
tmsConsignmentMaterial.setLineNumber(8);
tmsConsignmentMaterial.setVolumeUnit("千克");
tmsConsignmentMaterialList.add(tmsConsignmentMaterial);
consignmentInfo.setTmsConsignment(tmsConsignment);
consignmentInfo.setTmsConsignmentMaterials(tmsConsignmentMaterialList);
messageSender.sendMsg(AmqpConstant.NETWORK_LETTER_EXCHANGE,AmqpConstant.NETWORK_LETTER_ROUTING_RECEIVE,consignmentInfo);
i++;
return i + "";
}
消费消息测试类
/***
* 网络运单接收
* @param
*/
@MessageException(second = 1800l,retry = 3,milli = 2000L)
@RabbitListener(queues = AmqpConstant.NETWORK_LETTER_QUEUE_RECEIVE)
public void networdtest(Message message, Channel channel){
throw new BaseException("消费异常");
}
超过三次进入死信队列 代码如下
/***
* 死信队列接收
* @param
*/
@RabbitListener(queues = AmqpConstant.DEAD_LETTER_QUEUE_NAME)
public void asdf(Message message, Channel channel) throws IOException {
long tag = message.getMessageProperties().getDeliveryTag();
String json = new String(message.getBody());
String messageId = message.getMessageProperties().getMessageId();
ConsignmentInfo msg = JSONObject.parseObject(json, ConsignmentInfo.class);
System.out.println("进入死信队列,msg=="+msg.getTmsConsignment().getReceiverUserName()+",消息id为:"+messageId);
channel.basicAck(tag, false);
}
yml配置
# Spring
spring:
rabbitmq:
host: 127.0.0.0
port: 5672
username: admin
password: 123455
virtual-host: host
#确认消息已发送到交换机(Exchange)
publisher-confirms: true
#确认消息已发送到队列(Queue)
publisher-returns: true
connection-timeout: 15000
# 开启ack
listener:
direct:
acknowledge-mode: manual
simple:
#false 消息被拒绝以后不会重新进入队列,超过最大重试次数会进入死信队列
default-requeue-rejected: false
acknowledge-mode: manual #采取手动应答
#concurrency: 1 # 指定最小的消费者数量
#max-concurrency: 1 #指定最大的消费者数量
retur