RabbitMq手动确认+消息重试

需求与思路:
避免消费端消费消息失败之后重试再消费失败再重试的死循环,将重试计数放入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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值