近期,我们在生产环境中遭遇了一项库存服务问题——重复入库现象。经过深入排查,问题根源在于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());
}