rocketmq源码解析(rocketmq源码部署)

本文主要分析RocketMQ中如何保证消息有序的。 RocketMQ的版本为:4.2.0release。 一.时序图 还是老规矩,先把分析过程的时序图摆出来: 1.Producer发送顺序消息 2.Consumer接收顺序消息(一) 3.Consumer接收顺序消息(二) 二.源码分析-Producer发送顺序消息 1DefaultMQProducer#send:发送消息,入参中有自定义的消息队列…

文中关键分析RocketMQ中怎么确保消息有序的。

RocketMQ的版本号为:4.2.0 release。

一.时序图

或是规矩,先把分析全过程的时序图摆出:

1.Producer推送顺序消息

RocketMQ源码:有序消息分析

2.Consumer接受顺序消息(一)

RocketMQ源码:有序消息分析

3.Consumer接收顺序消息(二)

RocketMQ源码:有序消息分析

二.源码分析 – Producer推送顺序消息

1 DefaultMQProducer#send:发送消息,入参中有自定的消息序列选择符。

 // DefaultMQProducer#send
 public SendResult send(Message msg, MessageQueueSelector selector, Object arg)
 throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
 return this.defaultMQProducerImpl.send(msg, selector, arg);
 }

1.1 DefaultMQProducerImpl#makeSureStateOK:保证Producer的情况是运作情况-ServiceState.RUNNING。

 // DefaultMQProducerImpl#makeSureStateOK
 private void makeSureStateOK() throws MQClientException {
 if (this.serviceState != ServiceState.RUNNING) {
 throw new MQClientException(\"The producer service state not OK, \"  this.serviceState
   FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
 null);
 }
 }

1.2 DefaultMQProducerImpl#tryToFindTopicPublishInfo:依据Topic获得公布Topic使用的路由器信息内容。

 // DefaultMQProducerImpl#tryToFindTopicPublishInfo
 private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
 TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
 if (null == topicPublishInfo || !topicPublishInfo.ok()) {
 this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);// 为空则从 NameServer升级获得,false,不传到 defaultMQProducer
 topicPublishInfo = this.topicPublishInfoTable.get(topic);
 }
 if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {// 拥有路由器信息内容并且情况OK,则回到
 return topicPublishInfo;
 } else {
 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
 topicPublishInfo = this.topicPublishInfoTable.get(topic);
 return topicPublishInfo;
 }
 }

1.3 启用自定消息序列选择符的select方式。

 // DefaultMQProducerImpl#sendSelectImpl
 MessageQueue mq = null;
 try {
 mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
 } catch (Throwable e) {
 throw new MQClientException(\"select message queue throwed exception.\", e);
 }
 // Producer#main
 SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
 @Override
 public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
 Integer id = (Integer) arg;
 int index = id % mqs.size();
 return mqs.get(index);
 }
 }, orderId);

1.4 DefaultMQProducerImpl#sendKernelImpl:推送消息的关键完成方式。

 // DefaultMQProducerImpl#sendKernelImpl
 ......
 switch (communicationMode) {
 case SYNC:
 long costTimeSync = System.currentTimeMillis() - beginStartTime;
 if (timeout < costTimeSync) {
 throw new RemotingTooMuchRequestException(\"sendKernelImpl call timeout\");
 }
 sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
 brokerAddr,
 mq.getBrokerName(),
 msg,
 requestHeader,
 timeout - costTimeSync,
 communicationMode,
 context,
 this);
 break;
 ......

1.4.1 MQClientAPIImpl#sendMessage:推送消息。

 // MQClientAPIImpl#sendMessage
 ......
 switch (communicationMode) {// 依据推送消息的方式(同歩/多线程)挑选差异的方法,默认设置是同歩
 case SYNC:
 long costTimeSync = System.currentTimeMillis() - beginStartTime;
 if (timeoutMillis < costTimeSync) {
 throw new RemotingTooMuchRequestException(\"sendMessage call timeout\");
 }
 return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request);
 ......

1.4.1.1 MQClientAPIImpl#sendMessageSync:推送同歩消息。

 // MQClientAPIImpl#sendMessageSync
 private SendResult sendMessageSync(
 final String addr,
 final String brokerName,
 final Message msg,
 final long timeoutMillis,
 final RemotingCommand request
 ) throws RemotingException, MQBrokerException, InterruptedException {
 RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
 assert response != null;
 return this.processSendResponse(brokerName, msg, response);
 }

1.4.1.1.1 NettyRemotingClient#invokeSync:结构RemotingCommand,启用的形式是同歩。

 // NettyRemotingClient#invokeSync 
 RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
 if (this.rpcHook != null) {
 this.rpcHook.doAfterResponse(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
 }
 return response;

三.源码分析 – Consumer接受次序信息(一)

1 DefaultMQPushConsumer#registerMessageListener:把Consumer传到的信息窃听器添加到messageListener中。

 // DefaultMQPushConsumer#registerMessageListener
 public void registerMessageListener(MessageListenerOrderly messageListener) {
 this.messageListener = messageListener;
 this.defaultMQPushConsumerImpl.registerMessageListener(messageListener);
 }

1.1 DefaultMQPushConsumerImpl#registerMessageListener:把Consumer传到的信息窃听器添加到messageListenerInner中。

 // DefaultMQPushConsumerImpl#registerMessageListener
 public void registerMessageListener(MessageListener messageListener) {
 this.messageListenerInner = messageListener;
 }

2 DefaultMQPushConsumer#start:运行Consumer。

 // DefaultMQPushConsumer#start
 public void start() throws MQClientException {
 this.defaultMQPushConsumerImpl.start();
 }

2.1 DefaultMQPushConsumerImpl#start:启动ConsumerImpl。

 // DefaultMQPushConsumerImpl#start
 switch (this.serviceState) {
 case CREATE_JUST:// 刚建立
 ......
 if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {// 井然有序信息服务项目
 this.consumeOrderly = true;
 this.consumeMessageService = new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
 } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {// 高并发混乱信息服务项目
 this.consumeOrderly = false;
 this.consumeMessageService = new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
 }
 ......
 this.consumeMessageService.start();// 运行信息服务项目
 ......
 mQClientFactory.start();// 运行MQClientInstance
 ......

2.1.1 new
ConsumeMessageOrderlyService():结构次序信息服务项目。

 // ConsumeMessageOrderlyService#ConsumeMessageOrderlyService
 public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl,
 MessageListenerOrderly messageListener) {
 this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;
 this.messageListener = messageListener;
 this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();
 this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();
 this.consumeRequestQueue = new LinkedBlockingQueue<Runnable>();
 this.consumeExecutor = new ThreadPoolExecutor(// 主信息交易线程池,正常的实行接到的ConsumeRequest。线程同步
 this.defaultMQPushConsumer.getConsumeThreadMin(),
 this.defaultMQPushConsumer.getConsumeThreadMax(),
 1000 * 60,
 TimeUnit.MILLISECONDS,
 this.consumeRequestQueue,
 new ThreadFactoryImpl(\"ConsumeMessageThread_\"));
 this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(\"ConsumeMessageScheduledThread_\"));
 }

2.1.2
ConsumeMessageOrderlyService#start:运行线程池手机客户端案例。

 // DefaultMQPushConsumerImpl#start
 this.consumeMessageService.start();
 // ConsumeMessageOrderlyService#start
 public void start() {
 if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 @Override
 public void run() {
 ConsumeMessageOrderlyService.this.lockMQPeriodically();// 按时向broker推送大批量锁定现阶段已经交易的序列结合的信息
 }
 }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
 }
 }

2.1.2.1
ConsumeMessageOrderlyService#lockMQPeriodically:按时向broker推送大批量锁定现阶段已经交易的序列结合的信息。

2.1.2.1.1 RebalanceImpl#lockAll:锁定全部已经信息的序列。

 // ConsumeMessageOrderlyService#lockMQPeriodically
 if (!this.stopped) {
 this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll();
 }
 // RebalanceImpl#lockAll
 HashMap<String, Set<MessageQueue>> brokerMqs = this.buildProcessQueueTableByBrokerName();// 依据brokerName从processQueueTable获得已经交易的序列结合
 ......
 Set<MessageQueue> lockOKMQSet = this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);// 向Broker推送锁定线程池的命令
 for (MessageQueue mq : lockOKMQSet) {
 ProcessQueue processQueue = this.processQueueTable.get(mq);
 if (processQueue != null) {
 if (!processQueue.isLocked()) {
 log.info(\"the message queue locked OK, Group: {} {}\", this.consumerGroup, mq);
 }
 processQueue.setLocked(true);
 processQueue.setLastLockTimestamp(System.currentTimeMillis());
 }
 }
 ......

2.1.3 MQClientInstance#start:运行MQClientInstance。全过程较繁杂,放进大文章标题四中分析。

 // DefaultMQPushConsumerImpl#start
 mQClientFactory.start();

四.源码分析 – Consumer接受次序信息(二)

1 MQClientInstance#start:运行手机客户端案例MQClientInstance。

 // MQClientInstance#start
 synchronized (this) {
 switch (this.serviceState) {
 case CREATE_JUST:
 ......
 // Start pull service 运行获取信息服务项目
 this.pullMessageService.start();
 // Start rebalance service 启动消费端web服务服务项目
 this.rebalanceService.start();
 ......

1.1 PullMessageService#run:启动拉取消息服务项目。具体调用的是DefaultMQPushConsumerImpl的pullMessage方式。

 // PullMessageService#run
 public void run() {
 log.info(this.getServiceName()   \" service started\");
 while (!this.isStopped()) {
 try {
 PullRequest pullRequest = this.pullRequestQueue.take();
 this.pullMessage(pullRequest);
 } catch (InterruptedException ignored) {
 } catch (Exception e) {
 log.error(\"Pull Message Service Run Method exception\", e);
 }
 }
 log.info(this.getServiceName()   \" service end\");
 }
 // PullMessageService#pullMessage
 private void pullMessage(final PullRequest pullRequest) {
 final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
 if (consumer != null) {
 DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
 impl.pullMessage(pullRequest);// 调用DefaultMQPushConsumerImpl的pullMessage
 } else {
 log.warn(\"No matched consumer for the PullRequest {}, drop it\", pullRequest);
 }
 }

1.1.1.1 DefaultMQPushConsumerImpl#pullMessage:拉取消息。递交到
ConsumeMessageOrderlyService的线程池consumeExecutor中实行。

 // DefaultMQPushConsumerImpl#pullMessage
 ......
 PullCallback pullCallback = new PullCallback() {
 @Override
 public void onSuccess(PullResult pullResult) {
 switch (pullResult.getPullStatus()) {
 case FOUND:
 long prevRequestOffset = pullRequest.getNextOffset();
 pullRequest.setNextOffset(pullResult.getNextBeginOffset());
 long pullRT = System.currentTimeMillis() - beginTimestamp;
 ......
 DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
 pullResult.getMsgFoundList(),
 processQueue,
 pullRequest.getMessageQueue(),
 dispatchToConsume);
 ......

1.1.1.1.1.1.1 ConsumeRequest#run:解决消息消费的进程。

 // ConsumeMessageOrderlyService.ConsumeRequest#run
 List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
 ......
 long beginTimestamp = System.currentTimeMillis();
 ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
 boolean hasException = false;
 try {
 this.processQueue.getLockConsume().lock();
 if (this.processQueue.isDropped()) {
 log.warn(\"consumeMessage, the message queue not be able to consume, because it\'s dropped. {}\",this.messageQueue);
 break;
 }
 status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);// 具体消费消息的地区,调整消息窃听器的consumeMessage方式
 } catch (Throwable e) {
 log.warn(\"consumeMessage exception: {} Group: {} Msgs: {} MQ: {}\",
 RemotingHelper.exceptionSimpleDesc(e),
 ConsumeMessageOrderlyService.this.consumerGroup,
 msgs,messageQueue);
 hasException = true;
 } finally {
 this.processQueue.getLockConsume().unlock();
 }
 ......

1.2 RebalanceService#run:启动消息端web服务服务项目。

 // RebalanceService#run
 public void run() {
 log.info(this.getServiceName()   \" service started\");
 while (!this.isStopped()) {
 this.waitForRunning(waitInterval);
 this.mqClientFactory.doRebalance();
 }
 log.info(this.getServiceName()   \" service end\");
 }
 // MQClientInstance#doRebalance
 public void doRebalance() {
 for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
 MQConsumerInner impl = entry.getValue();
 if (impl != null) {
 try {
 impl.doRebalance();
 } catch (Throwable e) {
 log.error(\"doRebalance exception\", e);
 }
 }
 }
 }
 // DefaultMQPushConsumerImpl#doRebalance
 public void doRebalance() {
 if (!this.pause) {
 this.rebalanceImpl.doRebalance(this.isConsumeOrderly());
 }
 }

1.2.1.1.1 RebalanceImpl#doRebalance:web服务服务项目类解决。

 // RebalanceImpl#doRebalance
 public void doRebalance(final boolean isOrder) {
 Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
 if (subTable != null) {
 for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
 final String topic = entry.getKey();
 try {
 this.rebalanceByTopic(topic, isOrder);
 } catch (Throwable e) {
 if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
 log.warn(\"rebalanceByTopic Exception\", e);
 }
 }
 }
 }
 this.truncateMessageQueueNotMyTopic();
 }
 // RebalanceImpl#rebalanceByTopic
 switch (messageModel) {
 case BROADCASTING: {
 Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
 if (mqSet != null) {
 boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);// 依据Toipc除去queue
 if (changed) {
 this.messageQueueChanged(topic, mqSet, mqSet);
 log.info(\"messageQueueChanged {} {} {} {}\",
 consumerGroup,
 topic,
 mqSet,
 mqSet);
 }
 } else {
 ......
 // RebalanceImpl#updateProcessQueueTableInRebalance
 this.dispatchPullRequest(pullRequestList);// RebalancePushImpl派发消息

1.2.1.1.1.1.1.1 RebalancePushImpl#dispatchPullRequest:RebalancePushImpl分发。

 // RebalancePushImpl#dispatchPullRequest
 public void dispatchPullRequest(List<PullRequest> pullRequestList) {
 for (PullRequest pullRequest : pullRequestList) {
 this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);
 log.info(\"doRebalance, {}, add a new pull request {}\", consumerGroup, pullRequest);
 }
 }

五.汇总

对比Producer的推送步骤,Consumer的接受步骤略微繁杂一点。根据以上的源代码剖析,可以了解RocketMQ是如何确保信息的井然有序的:

1.根据ReblanceImp的lockAll方式,每过一段时间按时锁定现阶段交易摆正在交易的序列。设定当地序列ProcessQueue的locked特性为true。确保broker中的每一个线程池只相应一个交易端;

2.此外,交易端也是根据锁,确保每一个ProcessQueue只有一个进程交易。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

(0)
上一篇 2022年5月9日 上午11:48
下一篇 2022年5月9日 上午11:50

相关推荐

  • 华硕显示器怎么样(华硕PA329C显示器专业评测)

    对色彩要求苛刻的设计师、视频创作者等专业用户群体来说,非色彩表现优秀的专业显示器无法满足他们的需求。接下来要给大家介绍的是来自华硕的一款高端专业显示器——PA329C,作为一款售价近万元的产品,会给专业用户带来怎样的使用体验呢? 外观沉稳大气,接口丰富满足使用所需 华硕PA329C采用了典型的专业显示器外观设计,造型方方正正,屏幕底面一道金色装饰条可以算是点睛之笔,让整机外观更显沉稳大气。 显示器…

    2022年10月22日
    370
  • 淘宝网店可以转让吗,淘宝店铺变更店主方法

    我们要知道淘宝店是不容许转让的,但是有不少商家由于自己的经营不善和其它原因需要转让店铺,那么有什么办法能可靠的转让淘宝店铺呢,接下来就由小编带大家看一下。   淘宝店铺转让有两种方式,一种是熟人之间的私下交易,这种方式需要双方协商好,这种方式交易建议大家也是需要签订转让合同的,另一种转让方式就是通过第三方平台在线交易,这种就和中介是一样的需要抽取一定的费用,如果找到靠谱的第三方转让平台是没问题的。…

    2022年6月5日
    600
  • 如何写品牌软文,优秀的品牌型软文范例分享

    实际上,“到达”只是品牌传播的第一步,更为关键的一步是“建立联系”——通过文字、视频或者图片等方式,向读者传达特定的信息,甚至与读者产生共鸣。这就需要文字的力量。文字,是一篇文章的魂。 评判一篇品牌软文的好坏,首先是能否传达必要的信息,其次是在读者的大脑中留下印象,最后是能否让读者采取行动。 那么,如何写好一篇品牌软文?在准备为品牌撰写软文之前,如何着手写作?这里总结了几个步骤: 一、研读资料,把…

    2022年7月27日
    560
  • 国内奶粉什么牌子好,国产奶粉排行榜10强

    各位宝爸宝妈,这是新的一年啦,先祝大家新年有新的收获哟。 2020年第一期,先要完成2019年留下的一个“小尾巴”,就是更新2019年国产3段奶粉的横评(适用年龄1-3岁)。 至此,国行版/国产奶粉2019年最新横评就全部更新完毕。宝妈们可以在微信公众号后台输入关键词“国行”、“国产”查看,或输入关键词“1段”、“2段”、“3段”查看。 例行提醒:本期评测将以奶粉品牌为单位,逐一对各品牌旗下的各系…

    2022年10月14日
    8350
  • 摆地摊卖点什么小东西挣钱呢,不怕压货的地摊小生意

    地摊,生意虽小、门槛虽低,但也是一门生意。那什么样的商品,销量最好、利润比较高呢…… 看看麻辣社区成都论坛网友们的几个贴文分享参考一下吧! 网友“迷人玛利亚”: 这个我还真知道一点,毕竟曾经也摆过一年多地摊呢!地摊,生意虽小、门槛虽低,但毕竟也是一门生意,需要主要的地方有很多。那什么样的商品,销量最好、利润比较高呢!记住一点,地摊经济,主要是解决消费者的“刚需”,简单来说,就是在“衣、食、玩”这三…

    2022年8月16日
    920
  • 怎么查本机号码手机号,教你轻松一招就能搞定

    对于才刚刚有手机号码的人来说,记手机号码对于他们而言,可能是最困扰的事情了。因为手机号码的数字组成太过于长,并且都没有规律。这些都给手机号码的记忆造成困难。但是也正是因为手机号码的无序性以及多位数才能够满足我们一个国家那么多人的使用需要。因此,手机号码的那么如果我们在使用手机的过程中,想要和对方交换手机号码,但是又记不住我们自己的手机号码的话,我们有什么方法可以解决呢?大家别急,接下来小编就给大家…

    2022年9月3日
    740
  • dnf最强职业2019,盘点dnf最强五大职业

    DNF现在的职业虽然很多,但大体也就能分为两个类型,爆发型职业和续航型职业,最近被大家黑的特别惨的瞎子、以及别吹成神的弹药,就是典型的续航型职业。 而玩家基数最大的红王爷,则是爆发型职业的代表,今天阿森就来说说当前版本爆发最强的五个职业。 5、王小妹 王小妹是被许多玩家忽视的职业。但要知道现版本的天界人女大枪顶着能量一个一觉秒掉卢克,这个职业爆发真是强!都很强,王小妹作为一个天界人,又怎么可能会弱…

    2022年9月7日
    590
  • 校园创业项目推荐,大学校园新颖生意

    现在大学生在校创业的人也不在少数,那么,大学生在校创业最佳项目是什么?来为你介绍三个大学校园里面特有的投资项目,看看哪一个是您可以投资的吧! 大学生在校创业最佳项目——打印店 大学旁的打印店目标消费群毫无疑问应定在学生身上。现在大学生毕业前,都要精心准备自荐书,而且动辄几十份、上百份,以便到人才市场上找“婆家”。自荐书某种程度上代表一个人的素质,油印当然不行,而激光照排机、复印机价格较贵,一般人没…

    2022年6月6日
    650
  • 苹果微信聊天记录删除了怎么恢复,苹果微信记录免费找回方法

    误删微信聊天记录怎么恢复?苹果手机怎么找回删除的微信聊天记录?在删除微信聊天记录后,怎么恢复微信删除的记录是个很重要严肃的话题,我们在删除记录后,所要做的就是借助专业的数据恢复软件将删除的数据完整恢复,关于怎么恢复微信删除的记录的问题,大家经常会遇到。小编就给大家演示要恢复微信聊天记录的详细过程吧。 一、苹果手机怎么恢复微信删除的记录 1、要想使用迅捷微信聊天记录恢复软件来解决怎么恢复微信删除的记…

    2022年7月31日
    660
  • 怎么起商标名字,最新商标名字大全

    一个好的商标名字,就像是一张名片,让人一眼明了并且能够过目不忘。所以这也是为何近几年来人们对商标名字多下功夫的原因。接下来要和大家分享的就是如何取商标名字。 商标名字怎么取?商标取名的5大要点   商标名字怎么取   1.要精简   商标名字过长不易让人记住,名字最好是2-4个字左右。要有自己的独特性,不能与别人商标名雷同,让顾客感受到独特的服务。   2.不要太大众化   很多人觉得大众化的名字…

    2022年7月25日
    640

发表回复

登录后才能评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信