RabbitMQ系列(4)-基于反射原理实现手动确认模式下消息的自动接收

前言

RabbitMQ在消费消息时,有两种处理方式:
第一种:自动确认模式,也是默认模式,消息会在到达消费者之后立即从队列删除,不管消费端是否成功消费该条消息。这样情况下,如果消费者在消费消息过程中出现异常,或者网络等因素导致消费失败,那么消息就丢失了,适用于消息不太重要的场景,比如推送通知等;
第二种:手动确认模式,为了解决自动确认模式下造成消息丢失的情况,需要引入消息确认机制。有两种实现,一种是消息改为手动确认,也就是说只有消费者完成消息消费,手动确认消息,消息才回从队列中删除掉;另一种是事务控制。在实际应用中,一般使用手动确认模式。

手动确认模式,在接收消息后,处理业务时如果出现异常,那么消费者会不断接收到重发的消息。有时候消费者在出现某些异常时,比如代码错误,无法处理,并不希望继续接收到重发,这种情况下,手动确认模式也可以根据需求进行重发。

实现思路

在使用Spring amqp创建消费者并接收消息时,通常会用到下面两个接口。
在这里插入图片描述

根据选择的消息模式,实现上面两个接口之一,重写onMessage()方法来接收消息。
如果是自动确认模式,实现MessageListener接口,重写onMessage()方法处理消息;
如果是手动确认模式,实现ChannelAwareMessageListener接口,重写onMessage()方法处理消息,在onMessage()方法中添加处理消息的业务逻辑,可以根据情况自己选择是否重发,是否丢弃,重发几次等操作。

下面以具体代码讲述如何实现基于反射原理实现手动模式下更加智能地处理消息。

具体代码

以三个类或者枚举实现,代码结构如下:

  1. 定义抽象类:AbstractMessageConsumer,实现 ChannelAwareMessageListener 接口,重写onMessage()方法;
  2. 定义枚举:MessageBinding,将所有消费者都当做枚举值的一个对象,将交换机、队列、死信交换机、死信队列、绑定关系对象自动注入到spring容器中,自动注入参考:https://blog.csdn.net/renfujun2012/article/details/108641484
  3. 消费者类:AgencyChangeConsumer,消费信息。

下面是具体的代码实现:

  1. 抽象类:AbstractMessageConsumer,所有的消费者均继承该类,重写AbstractMessageConsumer类的onMessage(T message)方法和getMessageId(T message)方法,消费者的onMessage(T message)方法是真正处理消息的方法,getMessageId(T message)方法是获取消息内容某一字段的值,只是用于分布式锁的key值,没有其他业务。
@Slf4j
public abstract class AbstractMessageConsumer<T> implements ChannelAwareMessageListener {
  // messageBinding是枚举,包含交换机、队列、消息的类型等信息,通过MessageBinding中BindingInjector内部类的init()方法注入的。
  @Setter
  private MessageBinding messageBinding;
// 消息转换器
  private MessageConverter messageConverter = new SimpleMessageConverter();
  @Autowired
  private RedissonClient redisson;
  /**
   * 转换类型,通过反射注入.
   * 消费端接收的消息转化为java对象的类.
   */
// 消息类型的运行时类,通过MessageBinding内部类BindingInjector的init()方法注入
  private Class<T> convertClass;

  @Override
  public void onMessage(Message message, Channel channel) throws Exception {
    // 将消息转化为String类型
    String msgStr = messageConverter.fromMessage(message).toString();
    // 将消息转化为对象,T类型,所有的抽象方法继承父类AbstractMessageConsumer<T>,T类型就是子类继承时传入的参数类型,该类型含义为消息的类型,convertClass就是T的运行时类对象
    T msgDto = JSON.parseObject(msgStr, convertClass);
    RLock lock = redisson.getLock(RedisKeys.MQ_CONSUMER_LOCK
        .build(messageBinding.name() + getMessageId(msgDto)));
    log.info("收到消息: {} ,{} 尝试获取锁, 锁名称是: {}", messageBinding, message.toString(), lock.getName());

    try {
      if (lock.tryLock(0L, 30, TimeUnit.SECONDS)) {
        log.info("收到消息: {} ,{} 准备处理...", messageBinding, message.toString());
        try {
          // 执行消息处理业务,所有的消费者都重写了该方法
          onMessage(msgDto);
          //确认收到消息并处理完毕,第二个参数false表示只确认当前一个消息收到,true确认所有consumer获得的消息
          channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
          log.info("收到消息, 处理完毕: {}", message.toString());
        } catch (Exception e) {
          // 如果有异常,message.getMessageProperties().getRedelivered()判断消息是否已经再投投递了,也就是消息第一次处理失败,执行channel.basicNack()方法不确认消息已经接受,消息会再次进行消费,第二次的时候,message.getMessageProperties().getRedelivered()结果就是true。channel.basicReject()方法和channel.basicNack()方法第二个参数同样的意思
          if (message.getMessageProperties().getRedelivered()) {
            System.out.println("消息已重复处理失败,拒绝再次接收...");
// 拒绝消息,消息被拒绝后,就会从队列中被删除,如果队列绑定了死信交换机,那被拒绝的消息就会进入死信队列,否则就直接丢弃了
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); 
          } else {
// 第三个参数true表示重新回到队列,false表示丢弃消息,一般设为true
            System.out.println("消息即将再次返回队列处理...");
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); // requeue为是否重新回到队列
          }
          /*log.error("收到消息,处理失败: {}", message.toString());
          throw new RuntimeException(e);*/
        }

      } else {
        log.info("收到消息,有其它消费者处理了:{} ,当前锁信息: {}, 持有数量:{}", msgStr, lock.getName(),
            lock.getHoldCount());
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
      log.info("收到消息,释放锁 :{}", lock.getName());
    }

  }

  /**
   * 收到消息时的处理.
   */
  protected abstract void onMessage(T message);

  protected abstract Object getMessageId(T message);
}

  1. 枚举:MessageBinding
    该枚举定义了交换机、队列、绑定关系、adapter、container对象,实现消息的自动接收。所有的消费类都继承AbstractMessageConsumer类,重写onMessage()方法即可,消费者类里只有业务代码,不再有@Bean注解定义的交换机、队列、绑定关系等配置代码。
    如果需要新增消费者,只需要在MessageBinding增加相应的枚举值,编写消费类继承AbstractMessageConsumer类,重写onMessage()方法即可。
    发送消息只需要使用枚举的send()方法即可,不需要自建生产者类,例如发送财务同步数据消息:MessageBinding.THIRD_CHECK_INFO.send(message);一句代码即可,参数是Object类型。
public enum MessageBinding {
  // 接收消息的枚举值
  // 测试消费
  TEST_AMQP("exchange3", "queue3", TestConsumer.class, ExchangeType.TOPIC),
  // 机构属性变动消费
  AGENCY_CHANGE("company.fanout", "pay.agency.change", AgencyChangeConsumer.class, ExchangeType.FANOUT),

  // 发送消息的枚举值
  PILICOIN_AWARD_RESULT("pay.data.collection", "pay.goal.set", ExchangeType.TOPIC),
  THIRD_CHECK_INFO("pay.third.check", "third.sync", ExchangeType.TOPIC),
  ;

  // 静态变量rabbitTemplate,通过BindingInjector类的init()方法赋值
  // 如果不设置成static,这个属性就属于对象,需要加入到构造方法中.
  // 该变量用于发送mq消息使用
  private static AmqpTemplate rabbitTemplate;
  @Getter
  private String exchange;
  @Getter
  private String queue;
  // 消费者运行时类,所有的消费者都继承了AbstractMessageConsumer<T>抽象类,T为消息的接受对象dto
  private Class<? extends AbstractMessageConsumer> consumerClass;
  private ExchangeType exchangeType;

  MessageBinding(String exchange, String queue, ExchangeType exchangeType) {
    this.exchange = exchange;
    this.queue = queue;
    this.consumerClass = null;
    this.exchangeType = exchangeType;
  }

  MessageBinding(String exchange, String queue, Class<? extends AbstractMessageConsumer> consumerClass, ExchangeType exchangeType) {
    this.exchange = exchange;
    this.queue = queue;
    this.consumerClass = consumerClass;
    this.exchangeType = exchangeType;
  }


  /**
   * 发送消息.
   * 直接使用枚举类调用该方法即可发送消息.
   */
  public void send(Object message) {
    rabbitTemplate.convertAndSend(exchange, queue, JSON.toJSONString(message));
  }

  /**
   * 交换机类型.
   */
  private enum ExchangeType {
    TOPIC,
    FANOUT,
    ;
  }

  /**
   * 创建交换机、队列、绑定关系、adapter、container对象.
   * 该方法未创建死信交换机、死信队列对象,后期可以优化加上,这样如果消费者拒绝消息,可以路由到死信队列里,防止消息丢失.
   */
  @Component
  static class BindingInjector {

    @Autowired
    private ConfigurableApplicationContext context;
    @Autowired
    private AmqpTemplate rabbitTemplate;
    @Autowired
    private ConnectionFactory connectionFactory;

    @PostConstruct
    public void init() {
      // 为枚举的静态变量赋值
      MessageBinding.rabbitTemplate = rabbitTemplate;
      DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();

      for (MessageBinding mb : MessageBinding.values()) {
        //创建queue对象
        Queue queue = registerBinding(beanFactory, createBeanName(mb.name(), "Queue"), Queue.class,
            () -> new Queue(mb.queue));
        if (mb.exchangeType == ExchangeType.TOPIC) {
          //创建exchange对象
          TopicExchange exchange = registerBinding(beanFactory,
              createBeanName(mb.name(), "Exchange"),
              TopicExchange.class, () -> new TopicExchange(mb.exchange));
          //创建binding对象
          registerBinding(beanFactory, createBeanName(mb.name(), "Binding"), Binding.class,
              () -> BindingBuilder.bind(queue).to(exchange).with(mb.queue));
        } else {
          //创建exchange对象
          FanoutExchange fanoutExchange = registerBinding(beanFactory,
              createBeanName(mb.name(), "Exchange"),
              FanoutExchange.class, () -> new FanoutExchange(mb.exchange));
          //创建binding对象
          registerBinding(beanFactory, createBeanName(mb.name(), "Binding"), Binding.class,
              () -> BindingBuilder.bind(queue).to(fanoutExchange));
        }

        // 注册consumer消费者,如果是消息生产者,mb.consumerClass属性为null
        // 消费者需要和SimpleMessageListenerContainer container进行关联,这样才能交给程序自动监听
        // 关联后,消息接收统一走AbstractMessageConsumer实现的onMessage()方法
        if (mb.consumerClass != null) {
          // 获取consumer,根据类型从beanFactory中获取bean,多态形式接收
          AbstractMessageConsumer<?> consumer = beanFactory.getBean(mb.consumerClass);

          //注入反序列化类型
          // consumer.getClass().getGenericSuperclass()获取父类信息(包括泛型信息),该方法主要用于获取泛型信息
          ParameterizedType parameterizedType = (ParameterizedType) consumer.getClass().getGenericSuperclass();
          // 获取具体的泛型对象,consumer可能会有多个泛型,取第一个,因为项目里只有一个泛型
          // convertClass其实就是泛型T,各个消费者在继承父类的时候,已经传递了该泛型的类型,convertClass就是消息dto
          Class convertClass = (Class) parameterizedType.getActualTypeArguments()[0];
          try {
            // 为父类AbstractMessageConsumer的字段convertClass赋值
            Field field = consumer.getClass().getSuperclass()
                .getDeclaredField("convertClass");
            field.setAccessible(true);
            // 为AbstractMessageConsumer的convertClass属性赋值
            // field.set()方法的第一个参数是要赋值类的对象,第二个参数是字段的新值
            field.set(consumer, convertClass);
          } catch (Exception e) {
            e.printStackTrace();
          }

          Objects.requireNonNull(consumer);
          // 为AbstractMessageConsumer的messageBinding属性赋值
          consumer.setMessageBinding(mb);
          //注册adapter
          MessageListenerAdapter listenerAdapter = registerBinding(beanFactory,
              createBeanName(mb.name(), "ListenerAdapter"), MessageListenerAdapter.class,
              () -> new MessageListenerAdapter(consumer));

          //注册container
          registerBinding(beanFactory,
              createBeanName(mb.name(), "ListenerContainer"), SimpleMessageListenerContainer.class,
              () -> {
                SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
                container.setConnectionFactory(connectionFactory);
                container.setQueueNames(mb.queue);
                container.setMessageListener(listenerAdapter);
                //使用外部事务
                //container.setChannelTransacted(true);
                //手动确认
                container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
                return container;
              });
        }
      }
    }

    /**
     * 将对象交给sping管理.
     * 创建BeanDefination对象,并将对象放入DefaultListableBeanFactory的ConcurrentHashMap中管理,key是对象的名称,value是BeanDefination对象.
     * 该方法对象是手动创建好,交给spring管理,并不是spring自动创建的,单例模式.
     *
     * @param beanFactory bean工厂,实现了ConfigurableListableBeanFactory接口,最终都实现了BeanFactory顶级接口.
     * @param beanName 对象的名称,也就是ConcurrentHashMap中的key.
     * @param obj 对象的类型.
     * @param supplier 对象,lambda表达式形式传入,重写get()方法,返回一个T类型的对象.
     */
    @SuppressWarnings("unchecked")
    private <T> T registerBinding(DefaultListableBeanFactory beanFactory, String beanName,
                                  Class<T> obj, Supplier<T> supplier) {
      if (!beanFactory.containsBean(beanName)) {
        beanFactory.registerBeanDefinition(beanName,
            BeanDefinitionBuilder.genericBeanDefinition(obj, supplier).setScope("singleton")
                .getRawBeanDefinition());
      }
      return (T) beanFactory.getBean(beanName);
    }

    /**
     * 创建对象的名称.
     * 该方法创建的对象名称格式是:如果enumName = AGENCY_CHANGE,创建队列对象,则结果是:AgentQueueChangeQueueQueue.
     * 方法名不太有可读性,可以优化该方法,将名称变为:AgentChangeQueue 这种.
     */
    private String createBeanName(String enumName, String suffix) {
      StringBuilder sb = new StringBuilder();
      // 枚举name变为小写
      String str = enumName.toLowerCase();
      if (str.contains("_")) {
        String[] split = str.split("_");
        for (String s : split) {
          String upperTable = createBeanName(s, suffix);
          sb.append(upperTable);
        }
      } else {
        char[] ch = str.toCharArray();
        if (ch[0] >= 'a' && ch[0] <= 'z') {
          // 首字母由小写变大写
          ch[0] = (char) (ch[0] - 32);
        }
        sb.append(ch);
      }
      return sb.append(suffix).toString();
    }
  }
}

MessageBinding类的主要作用是:在项目启动时为每一个交换机、队列、绑定关系、adapter、container创建java对象,不然就需要在每一个消费者代码里创建这5个对象,在每一个生产者代码里创建交换机、队列、绑定关系3个对象,代码重复且繁琐,如果生产者和消费者有很多,那代码维护起来特别麻烦,代码冗余较多。
为什么要创建adapter、container对象呢,因为只有创建了这两个对象,才会执行AbstractMessageConsumer类重写的onMessage()方法。
采用上述方式代码简约、易懂,而且不需要创建生产者类,只需要用枚举调用send()方法即可发送消息,简洁好维护。

  1. 消费者类:AgencyChangeConsumer:
    继承AbstractMessageConsumer类,重写onMessag()方法,其中< AgencyChangeDto >就是消息的dto.
@Service
@Slf4j
public class AgencyChangeConsumer extends AbstractMessageConsumer<AgencyChangeDto> {

  @Override
  @Transactional(rollbackFor = Exception.class)
  protected void onMessage (AgencyChangeDto message) {
    log.info("onMessage,机构属性变动消费者,message={}", message);
    // 拿到消息message,下面写具体的业务代码

  }

  @Override
  protected Object getMessageId(AgencyChangeDto message) {
    return message.getAgencyId();
  }
}

  1. 总结:
    以上3个类配合使用,就可以完成消息的自动接收,消息发送使用枚举的send()方法。该模式可以动态的创建消息的配置对象,代码简洁好维护,不再需要额外的写配置类和生产者的类。
    枚举中没有创建死信交换机、死信队列对象,需要的话可以一并创建,并在消费队列中绑定死信交换机,这样被拒绝的消息就进入到死信队列中了。

消息的状态变化

手动模式下,在控制台中,队列中的消息分为三种状态:
在这里插入图片描述

Ready表示消息在队列中,可以正常投递,Unacked表示消息没有被消费者确认,Total表示Ready+Unacked消息的和。
Ready中的消息会一直被消费,加入在手动确认模式下,执行channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);,消息会被重新投递到队列中,也是Ready状态,而且是第一个消费,比如队列中有100个消息,应该先消费第一条,如果执行这句代码,则第一条消息会被重新投递到队列中,而且还是第一个。
Unacked中消息的产生,是因为在消费过程中产生异常,或者在获取分布式锁的时候没有获取到锁,消费就直接结束了,也就是没有确认消息,也没有拒绝消息,也没有不确认消息,消息就会变成Unacked状态,这种状态的消息不会再重新投递。当没有消费者连接该队列时,Unacked状态的消息会变成Ready状态,当消费者再次连接该队列时,消息会重新投递。也就是说,Unacked状态的消息要想变成Ready,只有2个方法,一个是RabbitMQ重启(前提是rabbitmq做了持久化),一个是消费者消费者重启服务。如果没有异常产生,不会出现Unacked消息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值