基于nacos实现的rocketMQ优雅启停

179 阅读2分钟

近期,我们在生产环境中遭遇了一项库存服务问题——重复入库现象。经过深入排查,问题根源在于MQ消费者在处理入库消息的过程中,刚好服务进行升级操作。这导致消息处理过程被迫中止,但此时已有部分数据已异步写入库存。当服务重启并重新开始消费MQ消息时,这部分已处理过的数据被再次处理,从而引发了重复入库问题。

为解决此问题,我们设计了一种简单而有效的策略:在服务进行更新前的下线阶段,利用对服务注册中心(此处以Nacos为例)服务下线事件的监听机制,确保RocketMQ消费者与服务同步下线。如此一来,即可确保在服务重启期间不存在正处于消费状态的消息,从根本上规避因服务中断导致的重复数据问题。

以下是该解决方案的具体流程图示:

具体的实现代码如下:

nacos服务上下线时间监听

@Component
@Slf4j
public class GracefulShutdown extends Subscriber<InstancesChangeEvent> implements ApplicationContextAware {

    private static boolean rocketMQIsRunning = true;
    private static boolean isInit = false;
    private static boolean isUp = false;
    @Value("${spring.application.name}")
    private String appName;
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    private ConfigurableApplicationContext applicationContext;

    @PostConstruct
    public void registerToNotifyCenter() {
        NotifyCenter.registerSubscriber(this);
    }

    @Override
    public synchronized void onEvent(InstancesChangeEvent event) {
        String serviceName = getServiceName(event.getServiceName());
        if (serviceName.equals(appName)) {
            log.info("【优雅关停】服务上下线事件: {}", JSON.toJSONString(event));
            boolean hostExists = event.getHosts().stream().anyMatch(host ->
                                                                    host.getIp().equals(nacosDiscoveryProperties.getIp()) &&
                                                                    host.getPort() == nacosDiscoveryProperties.getPort());
            if (!hostExists) {
                if (rocketMQIsRunning && isInit && isUp) {
                    log.info("【优雅关停】本服务已下线,开始销毁RocketMQ监听器");
                    rocketMQIsRunning = false;
                    isUp = false;
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException ignored) {
                    }
                    Map<String, AbstractConsumer> beansOfType = applicationContext.getBeansOfType(AbstractConsumer.class);
                    beansOfType.values().parallelStream().forEach(AbstractConsumer::shutdown);
                    log.info("【优雅关停】本服务已下线,完成销毁RocketMQ监听器");
                }
            } else {
                if (isInit && !isUp) {
                    log.info("【优雅关停】本服务已上线,开始启动RocketMQ监听器");
                    Map<String, AbstractConsumer> beansOfType = applicationContext.getBeansOfType(AbstractConsumer.class);
                    beansOfType.values().parallelStream().forEach(AbstractConsumer::start);
                    rocketMQIsRunning = true;
                    log.info("【优雅关停】本服务已上线,完成启动RocketMQ监听器");
                }
                isUp = true;
                isInit = true;
            }
        }
    }

    @Override
    public Class<? extends Event> subscribeType() {
        return InstancesChangeEvent.class;
    }

    public void rocketMQRunningCheck() {
        if (!rocketMQIsRunning) {
            throw new RuntimeException("【优雅关停】容器正在销毁不再消费");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = (ConfigurableApplicationContext) applicationContext;
    }
}

rocketMQ消费者配置

public void start() {
        // 日志打印赋值
        String message = StringUtils.isEmpty(message()) ? "【消费者】" : message();
        super.setProperties(rocketMqBaseProperties.getProperties());
        RocketMqProperties rocketMqProperties = rocketMqProperties();
        // GROUP_ID
        super.getProperties().put(PropertyKeyConst.GROUP_ID, rocketMqProperties.getGroup());
        if (rocketMqProperties.getConsumeThreadNums() != 0 && rocketMqProperties.getConsumeThreadNums() > 0) {
            super.getProperties().setProperty(PropertyKeyConst.ConsumeThreadNums, String.valueOf(rocketMqProperties.getConsumeThreadNums()));
        }
        //消息消费失败时的最大重试次数
        super.getProperties().put(PropertyKeyConst.MaxReconsumeTimes, "100");
        // 集群订阅方式设置(不设置的情况下,默认为集群订阅方式)
        super.getProperties().put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);

        //订阅关系
        Map<Subscription, MessageListener> subscriptionTable = new HashMap<>();
        Subscription subscription = new Subscription();
        subscription.setTopic(rocketMqProperties.getTopic());
        subscription.setExpression(rocketMqProperties.getTag());
        subscriptionTable.put(subscription, new AbstractMessageListener(mqExceptionService, retryAbleNoticeService) {

            @Override
            public boolean doBusiness(Message message) throws Exception {
                //消费者容器正在销毁不在消费新的消息
                gracefulShutdown.rocketMQRunningCheck();
                return doServiceBusiness(message);
            }

            @Override
            public int setMaxConsumeTimes() {
                return MAX_CONSUME_TIMES;
            }

            @Override
            public ProducerBean setProducer() {
                return producer;
            }

            @Override
            public String setDingRobotUrl() {
                return DING_ROBOT_URL;
            }

            @Override
            public String message() {
                return message;
            }
        });
        super.setSubscriptionTable(subscriptionTable);
        super.start();
        log.info("{}{},消费者Topic:{},消费者Tag:{},消费者Group:{}", message, "消费者启动成功", rocketMqProperties.getTopic(), rocketMqProperties.getTag(), rocketMqProperties.getGroup());
    }