可靠mq(一)——mq可靠消费

问题:

  • 解决问题的大方向
  • 为什么直接把消息发回消息队列
  • 数据保存在本地 VS 保存在mq.platform.ip.fdd
  • 保存在本地的好处:
  • 保存本地的缺点:
  • 保存在mq.platform.ip.fdd的好处:
  • 保存在mq.platform.ip.fdd的缺点:
  • 最终选择
  • 重试方式:定时任务 VS 起工作线程
  • 定时任务
  • 工作线程
  • mq-platform实现方式
  • 消费端
  • 服务端

问题

我们基本都采用自动应答,但是自动应答默认消费失败会将消息重新放回队列。

如果没有catch异常,则消息会被不断消费,但是又消费不成功,可能导致日志文件将硬盘撑爆。

为了避免这种情况,大家可能会在消费mq的业务代码最外围用try catch包住,等于消费永远成功(这种行为,等价于将defaultRequeueRejected设置为false)。

这种做法,在出现异常的时候,只是简单打一个异常日志,容易丢失mq消息,导致业务问题。

解决问题的大方向

大致的方案是:增加拦截器,消费失败的时候,持久化存储mq消息,到达指定的重消费时间,重新消费mq消息;到达最大重试次数依然不能消费成功,放弃消息。

方案流程图:

为什么直接把消息发回消息队列

如果选择将消息失败的消息保存在本地,那么重新消费mq消息,就会有两种选择:

1、将消息重放回队列,重新触发消息逻辑

2、在保存mq消息到本地的时候,也记录下消费mq消息的对象和方法,重新消费的时候,直接用反射调用处理方法

由于消费mq消息的代码千奇百怪,没办法简单的拿到消费mq消息的对象和方法,使用@RabbitListener注解的情况下,有点希望,但还涉及到数据转换等问题。

而直接将消息重发到队列就简单得多,发回原来的队列就行了。

数据保存在本地 VS 保存在mq.platform.ip.fdd

保存在本地的好处:

1、每个服务的数据都是独立的,不会相互影响

2、使用定时任务来实现重发mq消息的时候,大家可以各自控制频率

保存本地的缺点:

1、每个服务都需要在自己的业务库里创建一个保存消费失败mq的表,不方便升级,有的服务也没有数据库

2、重发mq如果依靠定时任务的方式,那每个服务都需要引入xxl-job

保存在mq.platform.ip.fdd的好处:

1、各个服务不需要建表,只是通过dubbo接口把消息保存到mq.platform.ip.fdd,,方便统一管理

2、各个服务不需要引入xxl-job这些额外的东西

保存在mq.platform.ip.fdd的缺点:

1、mq.platform.ip.fdd挂了,那么会丢失mq消息(不过告警信息里,可以把mq消息体带上,真出问也可以用告警信息里的内容手动重发)

2、业务服务都必须引入dubbo(我们本就就都会引入dubbo服务,感觉没影响)

最终选择

选择保存在mq.platform.ip.fdd,由mq.platform.ip.fdd统一来实现重试

重试方式:定时任务 VS 起工作线程

定时任务

可以选择在mq.platform.ip.fdd中引入xxl-job这个分布式任务调度,定时X每秒查询“待发送”的任务,但这会存在下面的问题:

1、X设置得太大,重试的时间准确性就降低,例如10秒才重试一次,那么最大有可能晚10秒重试

2、X设置得太小,可能上一次的任务还没执行完,下一次重试开始执行,把已经执行过一遍的任务重新执行

工作线程

可以在spring容器启动后,开启一个工作线程。线程在一个死循环里查询“待发送”的任务,然后发送出去。因为线程不停的工作,所以有这样的优势:

1、重发的时间较为准确

2、效率比定时任务高

不过用工作线程需要解决多个实例的问题,多个实体,会启动多个线程,多个线程一起工作的话,可能会导致重复发送的问题。

可以用比较简单的方式解决该问题:

1、引入zk分布式锁

2、线程开始工作前,先获取zk的锁,如果获取不到,就一直等待

3、获取到锁之后,进入while死循环,一直工作到容器退出才退出循环,释放zk的分布式锁

4、这其实就形成了一个主备,如果主服务不挂,就会一直由主服务的线程进行消息的重发;如果主服务挂了,备服务就会获取到zk的锁,成为主服务,然后进行工作

mq-platform实现方式

消费端:

在MqPlatformRabbitConsumerConfiguration配置类中,定义了mq.platform.ip.fdd接口的消费者,用于消费失败的时候,将mq消息保存到mq.platform.ip.fdd

/**
 * apache.dubbo的消费者定义
 */
@ConditionalOnClass(org.apache.dubbo.config.spring.context.annotation.EnableDubbo.class)
protected static class ApacheDubboConsumeServiceConfiguration {
    private String dubboApplicationName;
    private String dubboRegistryAddress;
 
    public ApacheDubboConsumeServiceConfiguration(@Value("${dubbo.application.name:}") String dubboApplicationName,
                                                  @Value("${dubbo.registry.address}") String dubboRegistryAddress,
                                                  MqPlatformProperties properties) {
        this.dubboApplicationName = StringUtils.isEmpty(dubboApplicationName) ? properties.getApplication() :
                dubboApplicationName;
        this.dubboRegistryAddress = dubboRegistryAddress;
    }
 
    /**
     * 定义mq-platform-server的ConsumeService消费者
     *
     * @return ConsumeService消费者
     */
    @Bean
    public ConsumeService consumeService() {
        com.alibaba.dubbo.config.ApplicationConfig applicationConfig = new com.alibaba.dubbo.config.ApplicationConfig();
        applicationConfig.setName(dubboApplicationName);
 
        com.alibaba.dubbo.config.ConsumerConfig consumerConfig = new com.alibaba.dubbo.config.ConsumerConfig();
        consumerConfig.setCheck(false);
        consumerConfig.setTimeout(3000);
        consumerConfig.setRetries(0);
        consumerConfig.setFilter("consumerCatFilter,default");
 
        com.alibaba.dubbo.config.RegistryConfig registryConfig = new com.alibaba.dubbo.config.RegistryConfig();
        registryConfig.setAddress(dubboRegistryAddress);
 
        com.alibaba.dubbo.config.ReferenceConfig<ConsumeService> referenceConfig =
            new com.alibaba.dubbo.config.ReferenceConfig<>();
        referenceConfig.setApplication(applicationConfig);
        referenceConfig.setConsumer(consumerConfig);
        referenceConfig.setRegistry(registryConfig);
        referenceConfig.setInterface(ConsumeService.class);
        return referenceConfig.get();
    }
}
 
/**
 * alibaba.dubbo的消费者定义
 */
@ConditionalOnClass(com.alibaba.dubbo.config.spring.context.annotation.EnableDubbo.class)
protected static class AlibabaDubboConsumeServiceConfiguration {
    private String dubboApplicationName;
    private String dubboRegistryAddress;
 
    public AlibabaDubboConsumeServiceConfiguration(@Value("${dubbo.application.name:}") String dubboApplicationName,
                                                  @Value("${dubbo.registry.address}") String dubboRegistryAddress,
                                                  MqPlatformProperties properties) {
        this.dubboApplicationName = StringUtils.isEmpty(dubboApplicationName) ? properties.getApplication() :
                dubboApplicationName;
        this.dubboRegistryAddress = dubboRegistryAddress;
    }
 
    /**
     * 定义mq-platform-server的ConsumeService消费者
     *
     * @return ConsumeService消费者
     */
    @Bean
    @ConditionalOnMissingBean(ConsumeService.class)
    public ConsumeService consumeService() {
        com.alibaba.dubbo.config.ApplicationConfig applicationConfig =
            new com.alibaba.dubbo.config.ApplicationConfig();
        applicationConfig.setName(dubboApplicationName);
 
        com.alibaba.dubbo.config.ConsumerConfig consumerConfig = new com.alibaba.dubbo.config.ConsumerConfig();
        consumerConfig.setCheck(false);
        consumerConfig.setTimeout(3000);
        consumerConfig.setRetries(0);
        consumerConfig.setFilter("consumerCatFilter,default");
 
        com.alibaba.dubbo.config.RegistryConfig registryConfig = new com.alibaba.dubbo.config.RegistryConfig();
        registryConfig.setAddress(dubboRegistryAddress);
 
        com.alibaba.dubbo.config.ReferenceConfig<ConsumeService> referenceConfig =
            new com.alibaba.dubbo.config.ReferenceConfig<>();
        referenceConfig.setApplication(applicationConfig);
        referenceConfig.setConsumer(consumerConfig);
        referenceConfig.setRegistry(registryConfig);
        referenceConfig.setInterface(ConsumeService.class);
        return referenceConfig.get();
    }
}

 

因为2.70之后的是apache版本,所以写了两份定义,兼容2.7.x和2.6.x

MqPlatformRabbitConsumerConfiguration中定义了可靠消费的拦截器RelyOperationsInterceptor,当有异常抛出(即消费失败的时候),会触发告警,然后就用上面定义的ConsumeService消费者,将消息保存到mq.platform.ip.fdd

/**
 * rabbit可靠消费(消费失败告警和保存)的配置
 */
@ConditionalOnProperty(prefix = "mq-platform.consumer.rely", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(MqPlatformProperties.class)
protected static class MqPlatformRabbitRelyConsumeConfiguration {
    private MqPlatformProperties properties;
    private MqPlatformConsumerRely relyProperties;
 
    public MqPlatformRabbitRelyConsumeConfiguration(
        MqPlatformProperties properties,
        MqPlatformConsumerRely relyProperties
    ) {
        this.properties = properties;
        this.relyProperties = relyProperties;
    }
 
    /**
     * 定义消费失败后保存消息的interceptor
     *
     * @param alarmProvider 告警bean提供者
     * @param consumeServiceProvider 消费服务提供者
     * @return 失败重试的interceptor
     */
    @Bean(BeanNameConstants.INTERNAL_RELY_OPERATIONS_INTERCEPTOR)
    @ConditionalOnMissingBean(RelyOperationsInterceptor.class)
    public RelyOperationsInterceptor retryOperationsInterceptor(
        ObjectProvider<Alarm> alarmProvider,
        ObjectProvider<ConsumeService> consumeServiceProvider
    ) {
        return new RelyOperationsInterceptor(properties.getApplication(), relyProperties.getMaxAttempts(),
            relyProperties.getInitialInterval().toMillis(), relyProperties.getMaxInterval().toMillis(),
            relyProperties.getMultiplier(), consumeServiceProvider, alarmProvider);
    }
}

服务端

服务端主要的业务代码在ConsumeRetryRunner,通过zk分布式锁实现主备,然后主服务不断查询数据库中的任务记录,重新发送mq消息

/**
 * @author wanglongzhao
 * @2020-02-10 11:14
 * description mq消息重试机制
 */
@Slf4j
@Component
public class ConsumeRetryRunner implements ApplicationRunner {
    private static final String CONSUME_RETRY_DISTRIBUTION_LOCK_KEY = "retryLockKey";
 
    /**
     * 使用分布式锁的时候,模块名称
     */
    @Value("${app.id: mq.platform.ip.fdd}")
    private String moduleName;
    /**
     * 每次循环最多处理多少个任务
     */
    @Value("${mq-platform-server.consume.retry-batch:30}")
    private int consumeRetryBatch = 30;
    /**
     * 没有任务的时候,工作线程的睡眠时间
     */
    @Value("${mq-platform-server.consume.sleep-time:1000}")
    private long sleepTime = 1000;
    /**
     * 检查异常任务的周期
     */
    @Value("${mq-platform-server.consume.check-period:5000}")
    private long checkPeriod = 5000;
 
    private volatile boolean working = true;
 
    @Autowired
    private LockManager lockManager;
    @Autowired
    private RabbitConsumeFailRecordManager manager;
    @Autowired
    private SendRabbitMqService sendRabbitMqService;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        AsyncHandlerUtil.excute(() -> {
            executeConsumeRetry();
            return Boolean.TRUE;
        });
    }
 
    public void stopWorking() {
        working = false;
    }
 
    private void executeConsumeRetry() {
        DistributionLock lock = createLock();
        while (!getDistributionLock(lock)) {}
        // 获取锁成功,开始执行任务
        long lastFetchRetryingJobTime = 0;
        try {
            while (working) {
                try {
                    long currentTime = System.currentTimeMillis();
                    // 每过{checkPeriod}毫秒,就检索“重试中”的任务,发送消息
                    if (currentTime - lastFetchRetryingJobTime > checkPeriod) {
                        lastFetchRetryingJobTime = currentTime;
                        fetchRetryingJobAndSendMessage();
                        continue;
                    }
                    // 检索“待重试”的任务,发送消息
                    fetchToRetryJobAndSendMessage();
                } catch (Exception e) {
                    try {
                        //延时3s
                        Thread.sleep(3000);
                    } catch (InterruptedException ignore) {}
                }
            }
        } finally {
            this.releaseDistributionLock(lock);
        }
    }
 
    private void fetchToRetryJobAndSendMessage() {
        //查询失败待重试的任务
        ListRabbitConsumeFailRecordFilter toRetryFilter = new ListRabbitConsumeFailRecordFilter();
 
        toRetryFilter.setRetryStatus(ConsumeRetryStatusEnum.TO_RETRY.getValue());
        toRetryFilter.setEndNextRetryTime(new Date());
        List<RabbitConsumeFailRecord> toRetryRecords = manager.listRecords(toRetryFilter, consumeRetryBatch);
        if (CollectionUtils.isEmpty(toRetryRecords)) {
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException ignore) {}
            return;
        }
        //更新数据库记录的重试状态,待重试更新为重试中
        toRetryRecords.forEach(record -> record.setRetryStatus(ConsumeRetryStatusEnum.RETRYING.getValue()));
        manager.updateBatchById(toRetryRecords);
        toRetryRecords.forEach(this::sendMessage2Queue);
    }
 
    private void fetchRetryingJobAndSendMessage() {
        // 查询一分钟前重试中(考虑到有可能上一次重试但是还未回调,所以增加一分钟前的过滤条件)的mq记录
        ListRabbitConsumeFailRecordFilter retryingFilter = new ListRabbitConsumeFailRecordFilter();
        retryingFilter.setRetryStatus(ConsumeRetryStatusEnum.RETRYING.getValue());
        retryingFilter.setEndNextRetryTime(DateUtils.addMinutes(new Date(), -1));
        List<RabbitConsumeFailRecord> retryingRecords = manager.listRecords(retryingFilter, consumeRetryBatch);
        if (CollectionUtils.isEmpty(retryingRecords)) {
            return;
        }
        retryingRecords.forEach(this::sendMessage2Queue);
    }
 
    private void sendMessage2Queue(RabbitConsumeFailRecord record) {
        try {
            Message message = ServerRabbitUtils.recover2Message(record);
            CorrelationData correlationData = new CorrelationData();
            correlationData.setId(record.getRecordId().toString());
            sendRabbitMqService.send(record.getVirtualHost(), record.getQueue(), message, correlationData);
        } catch (Throwable e) {
            log.error("fail to requeue message, record=" + JSON.toJSONString(record), e);
        }
    }
 
    private DistributionLock createLock() {
        LockInfo lockInfo = new LockInfo();
        lockInfo.setId(CONSUME_RETRY_DISTRIBUTION_LOCK_KEY);
        lockInfo.setModule(moduleName);
        //单位:ms 超时获取时间
        lockInfo.setWaitTime(5000);
        lockInfo.setLockProvider(LockProviderTypeEnum.ZOOKEEPER);
        return lockManager.createLock(lockInfo);
    }
 
    private Boolean getDistributionLock(DistributionLock lock) {
        try {
            while (!lock.tryLock()) {
            }
        } catch (Exception e) {
            log.error("分布式锁获取失败", e);
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }
 
    private void releaseDistributionLock(DistributionLock lock) {
        lock.unlock();
    }
 
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章