当前位置: 首页 > news >正文

RocketMq源码分析(九)--顺序消息

文章目录

  • 一、顺序消息
  • 二、顺序消息消费过程
    • 1、消息队列负载
    • 2、消息拉取
    • 3、消息消费
    • 4、消息进度存储
  • 三、总结

一、顺序消息

  RocketMq在同一个队列中可以保证消息被顺序消费,所以如果要做到消息顺序消费,可以将消费主题(topic)设置成一个队列。

二、顺序消息消费过程

  同普通消息一样,顺序消息消费需要经历4个步骤:消息队列负载、消息拉取、消息消费、消息进度存储。

1、消息队列负载

  消息队列负载由RebalanceService线程实现,每隔20s对消息队列重新负载。

//org.apache.rocketmq.client.impl.consumer.RebalanceService#run@Overridepublic void run() {log.info(this.getServiceName() + " service started");//20s重新负载一次while (!this.isStopped()) {this.waitForRunning(waitInterval);this.mqClientFactory.doRebalance();}log.info(this.getServiceName() + " service end");}

在集群模式下,同一个消费组内的消费者共同承担其订阅topic的消息队列的消费,同一个消息队列在同一时刻只能被组内一个消费者消费,一个消费者同一时刻可以分配多个消费队列

  在经过消息队列负载时,会创建新的消息拉取任务(PullRequest),如果判断是顺序消息消费(isOrder=true)时,需要向broker发起锁定该消息队列的请求(this.lock(mq)),如果锁定失败,则跳过,在下一次负载时再尝试加锁。

//org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalanceprivate boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet,final boolean isOrder) {boolean changed = false;//遍历原来的缓存列表Iterator<Entry<MessageQueue, ProcessQueue>> it = this.processQueueTable.entrySet().iterator();while (it.hasNext()) {Entry<MessageQueue, ProcessQueue> next = it.next();MessageQueue mq = next.getKey();ProcessQueue pq = next.getValue();if (mq.getTopic().equals(topic)) {//新的结果中不包含原来缓存中的队列,停止消费if (!mqSet.contains(mq)) {pq.setDropped(true);if (this.removeUnnecessaryMessageQueue(mq, pq)) {it.remove();changed = true;log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq);}} else if (pq.isPullExpired()) {switch (this.consumeType()) {case CONSUME_ACTIVELY:break;case CONSUME_PASSIVELY:pq.setDropped(true);if (this.removeUnnecessaryMessageQueue(mq, pq)) {it.remove();changed = true;log.error("[BUG]doRebalance, {}, remove unnecessary mq, {}, because pull is pause, so try to fixed it",consumerGroup, mq);}break;default:break;}}}}//启动新增加的消费队列List<PullRequest> pullRequestList = new ArrayList<PullRequest>();for (MessageQueue mq : mqSet) {if (!this.processQueueTable.containsKey(mq)) {if (isOrder && !this.lock(mq)) {log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);continue;}this.removeDirtyOffset(mq);ProcessQueue pq = new ProcessQueue();long nextOffset = -1L;try {nextOffset = this.computePullFromWhereWithException(mq);} catch (Exception e) {log.info("doRebalance, {}, compute offset failed, {}", consumerGroup, mq);continue;}if (nextOffset >= 0) {ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);if (pre != null) {log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);} else {//创建新的PullRequest并加入到pullRequestService中log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);PullRequest pullRequest = new PullRequest();pullRequest.setConsumerGroup(consumerGroup);pullRequest.setNextOffset(nextOffset);pullRequest.setMessageQueue(mq);pullRequest.setProcessQueue(pq);pullRequestList.add(pullRequest);changed = true;}} else {log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);}}}this.dispatchPullRequest(pullRequestList);return changed;}

2、消息拉取

  消息拉取由PullMessageService线程处理,当有消息拉取请求进来时,马上处理拉取消息,否则阻塞等待拉取请求。

//org.apache.rocketmq.client.impl.consumer.PullMessageService#run@Overridepublic void run() {log.info(this.getServiceName() + " service started");while (!this.isStopped()) {try {//取一个PullRequest消息拉取任务,如果pullRequestQueue为空,则阻塞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");}
//org.apache.rocketmq.client.impl.consumer.PullMessageService#pullMessageprivate void pullMessage(final PullRequest pullRequest) {//从MQClientInstance中获取内部实现类MQConsumerInnerfinal MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());if (consumer != null) {//强转换成PUSH消息消费服务,然后消费消息DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;impl.pullMessage(pullRequest);} else {log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);}}

  处理消息拉取任务使用DefaultMQPushConsumerImpl实例。消息队列处于加锁状态时,处理消息拉取请求,否则将拉取请求放入延迟队列延迟3s再处理

//org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessagepublic void pullMessage(final PullRequest pullRequest) {//....代码省略if (!this.consumeOrderly) {//非顺序性消费//....代码省略} else {//顺序性消费if (processQueue.isLocked()) {//如果消息处理队列 锁住状态if (!pullRequest.isPreviouslyLocked()) {long offset = -1L;try {//从负载中计算当前已拉取的消息进度(偏移量)offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue());} catch (Exception e) {this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);log.error("Failed to compute pull offset, pullResult: {}", pullRequest, e);return;}//pullRequest下条消息偏移量比较大说明当前繁忙boolean brokerBusy = offset < pullRequest.getNextOffset();log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",pullRequest, offset, brokerBusy);if (brokerBusy) {log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",pullRequest, offset);}pullRequest.setPreviouslyLocked(true);pullRequest.setNextOffset(offset);}} else {//消息处理队列非锁住状态则延迟3s再加入拉取队列this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);log.info("pull message later because not locked in broker, {}", pullRequest);return;}}//...代码省略}

3、消息消费

  消息消费由实现类ConsumeMessageOrderlyService处理,构造方法如下:

//org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#ConsumeMessageOrderlyServicepublic 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>();String consumeThreadPrefix = null;if (consumerGroup.length() > 100) {consumeThreadPrefix = new StringBuilder("ConsumeMessageThread_").append(consumerGroup.substring(0, 100)).append("_").toString();} else {consumeThreadPrefix = new StringBuilder("ConsumeMessageThread_").append(consumerGroup).append("_").toString();}//消费线程池this.consumeExecutor = new ThreadPoolExecutor(this.defaultMQPushConsumer.getConsumeThreadMin(),this.defaultMQPushConsumer.getConsumeThreadMax(),1000 * 60,TimeUnit.MILLISECONDS,this.consumeRequestQueue,new ThreadFactoryImpl(consumeThreadPrefix));//任务调度线程池this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_"));}

  启动时,如果是集群模式,则默认每隔20s(ProcessQueue.REBALANCE_LOCK_INTERVAL)执行一次锁定分配给自己的消费队列,只有标记队列锁定成功,消息拉取任务才可以执行。

//org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#startpublic void start() {if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {try {ConsumeMessageOrderlyService.this.lockMQPeriodically();} catch (Throwable e) {log.error("scheduleAtFixedRate lockMQPeriodically exception", e);}}}, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);}}

  上述代码中,lockMQPeriodically()最终调用RebalanceImpl#lockAll方法执行队列加锁,同时也包含对列解锁,具体步骤如下:

  • 1、获取所有broker中的消息队列brokerMqs,然后逐个遍历
  • 2、根据brokerName找到主节点(findBrokerResult),向该节点发送锁定消息队列请求this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ,得到锁定成功的队列集合lockOKMQSet
  • 3、遍历锁定成功的队列集合lockOKMQSet,将队列加锁状态以及加锁时间更新到本地队列缓存表this.processQueueTable
  • 4、其余不在锁定队列集合lockOKMQSet中的队列,设置加锁状态为false,即解锁。这将会暂定对该消息队列的消息拉取以及消费
    public void lockAll() {//构建消息处理队列HashMap<String, Set<MessageQueue>> brokerMqs = this.buildProcessQueueTableByBrokerName();Iterator<Entry<String, Set<MessageQueue>>> it = brokerMqs.entrySet().iterator();while (it.hasNext()) {Entry<String, Set<MessageQueue>> entry = it.next();final String brokerName = entry.getKey();final Set<MessageQueue> mqs = entry.getValue();if (mqs.isEmpty())continue;//从订阅信息查找broker主节点FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true);if (findBrokerResult != null) {LockBatchRequestBody requestBody = new LockBatchRequestBody();requestBody.setConsumerGroup(this.consumerGroup);requestBody.setClientId(this.mQClientFactory.getClientId());requestBody.setMqSet(mqs);try {//向主broker发送锁定消息队列,返回成功被当前消费者锁定的队列Set<MessageQueue> lockOKMQSet =this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);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());}}for (MessageQueue mq : mqs) {if (!lockOKMQSet.contains(mq)) {//如果返回的锁定队列不包含这个队列,则设置加锁状态为falseProcessQueue processQueue = this.processQueueTable.get(mq);if (processQueue != null) {processQueue.setLocked(false);log.warn("the message queue locked Failed, Group: {} {}", this.consumerGroup, mq);}}}} catch (Exception e) {log.error("lockBatchMQ exception, " + mqs, e);}}}}

  在消息拉取任务完成拉取后,回调处理触发消息消费(DefaultMQPushConsumerImpl#pullMessage中PullCallback代码),此时创建顺序消息消费任务(ConsumeRequest )并放入线程池中。

//org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#submitConsumeRequest@Overridepublic void submitConsumeRequest(final List<MessageExt> msgs,final ProcessQueue processQueue,final MessageQueue messageQueue,final boolean dispathToConsume) {if (dispathToConsume) {ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);this.consumeExecutor.submit(consumeRequest);}}

4、消息进度存储

  消息消费任务线程ConsumeRequest 消费完成后,更改消息消费进度。值得注意的是,在上述提到消息拉取后,提交消费任务,而消费任务并不止是消费本次拉取的消息,而是消费队列中所有的未处理消息。

//org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.ConsumeRequest#run@Overridepublic void run() {if (this.processQueue.isDropped()) {log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);return;}final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);synchronized (objLock) {if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {//广播模式:直接消费;集群模式:process必须被锁定并且未超时final long beginTime = System.currentTimeMillis();for (boolean continueConsume = true; continueConsume; ) {if (this.processQueue.isDropped()) {log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);break;}if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())&& !this.processQueue.isLocked()) {log.warn("the message queue not locked, so consume later, {}", this.messageQueue);ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);break;}if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())&& this.processQueue.isLockExpired()) {log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);break;}long interval = System.currentTimeMillis() - beginTime;if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);break;}final int consumeBatchSize =ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();//按顺序取出consumeBatchSize条消息List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());if (!msgs.isEmpty()) {final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);ConsumeOrderlyStatus status = null;ConsumeMessageContext consumeMessageContext = null;if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {//执行钩子函数consumeMessageContext = new ConsumeMessageContext();consumeMessageContext.setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup());consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());consumeMessageContext.setMq(messageQueue);consumeMessageContext.setMsgList(msgs);consumeMessageContext.setSuccess(false);// init the consume context typeconsumeMessageContext.setProps(new HashMap<String, String>());ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);}long beginTimestamp = System.currentTimeMillis();ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;boolean hasException = false;try {//申请消息消费锁this.processQueue.getConsumeLock().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);} catch (Throwable e) {log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s",RemotingHelper.exceptionSimpleDesc(e),ConsumeMessageOrderlyService.this.consumerGroup,msgs,messageQueue), e);hasException = true;} finally {this.processQueue.getConsumeLock().unlock();}if (null == status|| ConsumeOrderlyStatus.ROLLBACK == status|| ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {log.warn("consumeMessage Orderly return not OK, Group: {} Msgs: {} MQ: {}",ConsumeMessageOrderlyService.this.consumerGroup,msgs,messageQueue);}long consumeRT = System.currentTimeMillis() - beginTimestamp;if (null == status) {if (hasException) {returnType = ConsumeReturnType.EXCEPTION;} else {returnType = ConsumeReturnType.RETURNNULL;}} else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) {returnType = ConsumeReturnType.TIME_OUT;} else if (ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {returnType = ConsumeReturnType.FAILED;} else if (ConsumeOrderlyStatus.SUCCESS == status) {returnType = ConsumeReturnType.SUCCESS;}if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name());}if (null == status) {status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;}if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {consumeMessageContext.setStatus(status.toString());consumeMessageContext.setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status);ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);}ConsumeMessageOrderlyService.this.getConsumerStatsManager().incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);} else {continueConsume = false;}}} else {if (this.processQueue.isDropped()) {log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);return;}ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);}}}}

  通过ConsumeRequest#run方法可以看出,顺序消费任务有如下处理步骤:

  • 1、获取消息队列的锁对象,加锁成功后(synchronized (objLock))才可进行消费。这里是对整个消息队列加锁,也说明同一时刻队列只会被一个线程消费
  • 2、如果是广播模式,直接进入消费;集群模式则需要进行判断,如果消息队列已被锁定并且锁未超时,进入消费,否则将消息队列放入任务调度线程池中延时100ms处理
  • 3、进入循环消费,当处理队列processQueue标示为丢弃,则结束本次消费;当处于集群模式下,处理队列没有加锁或者加锁超时(距离上一次加锁时间超过阈值REBALANCE_LOCK_MAX_LIVE_TIME,默认30s),延时10ms处理,结束本次任务;当消费时长interval超过阈值MAX_TIME_CONSUME_CONTINUOUSLY,默认60s,延时10ms处理,结束本次任务
  • 4、按顺序取出consumeBatchSize(默认1)条消息,如果没有消息,则结束循环消费,结束本次消费任务
  • 5、执行消息前置钩子函数
  • 6、申请消息消费锁,调用消息监听器,执行业务具体消费逻辑,获取消费结果status
  • 7、根据消费结果status,计算消费返回结果returnType,如果存在钩子函数,则将返回结果存入供后置钩子函数使用
  • 8、处理消费结果,获取消息待更新进度并更新,获取是否继续消费结果continueConsume
//org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#processConsumeResultif (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {//更新消息消费进度this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);}

三、总结

1、消息顺序消费,需要将消费主题(topic)设置成一个队列
2、顺序消息消费需要经历4个步骤:消息队列负载、消息拉取、消息消费、消息进度存储

相关文章:

RocketMq源码分析(九)--顺序消息

文章目录 一、顺序消息二、顺序消息消费过程1、消息队列负载2、消息拉取3、消息消费4、消息进度存储 三、总结 一、顺序消息 RocketMq在同一个队列中可以保证消息被顺序消费&#xff0c;所以如果要做到消息顺序消费&#xff0c;可以将消费主题&#xff08;topic&#xff09;设置…...

Windows下nginx的启动,重启,关闭等功能bat脚本

echo off rem 提供Windows下nginx的启动&#xff0c;重启&#xff0c;关闭功能echo begincls ::ngxin 所在的盘符 set NGINX_PATHG:::nginx 所在目录 set NGINX_DIRG:\projects\nginx-1.24.0\ color 0a TITLE Nginx 管理程序增强版CLSecho. echo. ** Nginx 管理程序 *** echo.…...

Python 字典:dic = {} 和 dic = defaultdict(list)之间的区别

d defaultdict(list) 和 d {} 在Python中代表了两种不同类型的字典初始化方式&#xff0c;它们之间有几个关键的区别&#xff1a; 1、类型 d defaultdict(list)&#xff1a;这里使用的是 collections 模块中的 defaultdict 类。它是一个字典的子类&#xff0c;提供了一个默…...

绘图 Seaborn 10个示例

绘图 Seaborn 是什么安装使用显示中文及负号散点图箱线图小提琴图堆叠柱状图分面绘图分类散点图热力图成对关系图线图直方图 是什么 Seaborn 是一个Python数据可视化库&#xff0c;它基于Matplotlib。Seaborn提供了高级的绘图接口&#xff0c;可以用来绘制各种统计图形&#xf…...

airserver mac 7.27官方破解版2024最新安装激活图文教程

airserver mac 7.27官方破解版是一款好用的airplay投屏工具&#xff0c;可以轻松将ios荧幕镜像&#xff08;airplay&#xff09;至mac上&#xff0c;在mac平台上实现视频、音频、幻灯片等文件资源的接收及投放演示操作&#xff0c;解决iphone或ipad的屏幕录像问题&#xff0c;满…...

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《考虑移动式储能调度的配电网灾后多源协同孤岛运行策略》

这篇文章的标题表明研究的主题是在配电网发生灾害后&#xff0c;采用一种策略来实现多源协同孤岛运行&#xff0c;并在这个过程中特别考虑了移动式储能的调度。 让我们逐步解读标题的关键词&#xff1a; 考虑移动式储能调度&#xff1a; 文章关注的焦点之一是移动式储能系统的…...

Spring Boot 优雅地处理重复请求

前 言 对于一些用户请求&#xff0c;在某些情况下是可能重复发送的&#xff0c;如果是查询类操作并无大碍&#xff0c;但其中有些是涉及写入操作的&#xff0c;一旦重复了&#xff0c;可能会导致很严重的后果&#xff0c;例如交易的接口如果重复请求可能会重复下单。 重复的场…...

TailwindCSS 多主题色配置

TailwindCSS 多主题色配置 现在大多数网站都支持主题色变换&#xff0c;比如切换深色模式。那么我们该如何进行主题色配置呢&#xff1f; tailwind dark tailwind 包含一个 dark变体&#xff0c;当启用深色模式时&#xff0c;可以为网站设置不同样式 <div class"bg-whi…...

Vue3:表格单元格内容由:图标+具体内容 构成

一、背景 在Vue3项目中&#xff0c;想让单元格的内容是由 &#xff1a;图标具体内容组成的&#xff0c;类似以下效果&#xff1a; 二、图标 Element-Plus 可以在Element-Plus里面找是否有符合需求的图标iconfont 如果Element-Plus里面没有符合需求的&#xff0c;也可以在这…...

【项目日记(一)】高并发内存池项目介绍

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:项目日记-高并发内存池⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习C   &#x1f51d;&#x1f51d; 项目日记 1. 前言2. 什么是高并发内存池…...

4-Docker命令之docker commit

1.docker commit介绍 docker commit命令是用于根据docker容器的改变创建一个新的docker镜像 2.docker commit用法 docker commit [参数] container [repository[:tag]] [rootcentos79 ~]# docker commit --helpUsage: docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG…...

RabbitMQ学习笔记10 综合实战 实现新商家规定时间内上架商品检查

配置文件&#xff1a; 记住添加这个。 加上这段代码&#xff0c;可以自动创建队列和交换机以及绑定关系。 我们看到了我们创建的死信交换机和普通队列。 我们可以看到我们队列下面绑定的交换机。 我们创建一个controller包进行测试: 启动&#xff1a; 过一段时间会变成死信队列…...

Project Euler 865 Triplicate Numbers(线性dp)

题目 能通过每次消除3个一样的数字&#xff0c;最终把数字消成空的数字是合法的&#xff0c; 求串长度不超过n的&#xff0c;没有前导0的数字中&#xff0c;合法的数字的个数 n10000&#xff0c;答案对998244353取模&#xff0c;只需要输出数字 思路来源 乱搞AC 题解 暴力…...

计算机网络测试题第二部分

前言:如果没有做在线测试请自主独立完成&#xff0c;本篇文章只作为学习计算机网络的参考&#xff0c;题库中的题存在一定错误和不完整&#xff0c;请学习时&#xff0c;查找多方书籍论证&#xff0c;独立思考&#xff0c;如果存在疑虑可以评论区讨论。查看时&#xff0c;请分清…...

linux 15day apache apache服务安装 httpd服务器 安装虚拟主机系统 一个主机 多个域名如何绑定

目录 一、apache安装二、访问控制总结修改默认网站发布目录 三、虚拟主机 一、apache安装 [rootqfedu.com ~]# systemctl stop firewalld [rootqfedu.com ~]# systemctl disable firewalld [rootqfedu.com ~]# setenforce 0 [rootqfedu.com ~]# yum install -y httpd [rootqfe…...

Linux和Windows环境下如何使用gitee?

1. Linux 1.1 创建远程仓库 1.2 安装git sudo yum install -y git 1.3 克隆远程仓库到本地 git clone 地址 1.4 将文件添加到git的暂存区&#xff08;git三板斧之add&#xff09; git add 文件名 # 将指定文件添加到git的暂存区 git add . # 添加新文件和修改过的…...

Docker安装教程

docker官网 1.卸载旧版 yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine2.配置Docker的yum库 安装yum工具 yum install -y yum-utils配置Docker的yum源 yum-config-ma…...

【PWN】学习笔记(二)【栈溢出基础】

课程教学 课程链接&#xff1a;https://www.bilibili.com/video/BV1854y1y7Ro/?vd_source7b06bd7a9dd90c45c5c9c44d12e7b4e6 课程附件&#xff1a; https://pan.baidu.com/s/1vRCd4bMkqnqqY1nT2uhSYw 提取码: 5rx6 C语言函数调用栈 一个栈帧保存的是一个函数的状态信息&…...

02-Nacos和Eureka的区别与联系

Nacos和Eureka的区别 联系 Nacos和Eureka整体结构类似: 都支持服务注册, 服务拉取, 采用心跳方式对服务提供者做健康监测的功能 区别 Nacos支持服务端主动检测服务提供者状态: 临时实例采用心跳模式,非临时实例采用主动检测模式但对服务器压力比较大(不推荐) 心跳模式: 服务…...

常见的Linux系统版本

在介绍常见的Linux系统版本之前&#xff0c;首先需要区分Linux系统内核与Linux发行套件系统的不同。Linux系统内核指的是一个由Linus Torvalds负责维护&#xff0c;提供硬件抽象层、硬盘及文件系统控制及多任务功能的系统核心程序。而Linux发行套件系统是我们常说的Linux操作系…...

基于JavaWeb+SSM+Vue微信小程序的科创微应用平台系统的设计和实现

基于JavaWebSSMVue微信小程序的科创微应用平台系统的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 Lun文目录 1系统概述 1 1.1 研究背景 1 1.2研究目的 1 1.3系统设计思想 1 2相关技术…...

【Spring Boot 源码学习】ApplicationListener 详解

Spring Boot 源码学习系列 ApplicationListener 详解 引言往期内容主要内容1. 初识 ApplicationListener2. 加载 ApplicationListener3. 响应应用程序事件 总结 引言 书接前文《初识 SpringApplication》&#xff0c;我们从 Spring Boot 的启动类 SpringApplication 上入手&am…...

HCIP---RSTP/MSTP

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 STP协议虽然能够解决环路问题&#xff0c;但是收敛速度慢&#xff0c;影响了用户通信质量。IEEE于2001年发布的802.1w标准定义了快速生成树协议RSTP&#xff08;Rapid Spanning-Tree Proto…...

探索开源游戏的乐趣与无限可能 | 开源专题 No.47

CleverRaven/Cataclysm-DDA Stars: 9.0k License: NOASSERTION Cataclysm&#xff1a;Dark Days Ahead 是一个回合制的生存游戏&#xff0c;设定在一个后启示录世界中。尽管有些人将其描述为 “僵尸游戏”&#xff0c;但 Cataclysm 远不止于此。在这个残酷、持久、程序生成的世…...

springboot_3.2_freemark_基础环境配置

springboot_3.2_freemark_基础环境配置 一、前言二、环境三、相关资料四、目标五、默认配置项六、构建springboot 3.2项目6.1 pom.xml 内容&#xff1a;6.2 启动类6.3 添加ftlh模板6.4 controller内容6.5 bootstrap.yml配置 七、总结 一、前言 FreeMarker 是一款模板引擎&…...

【MySQL】MySQL 在 Centos 7环境安装教程

文章目录 1.卸载不要的环境2.检查系统安装包3.获取mysql官方yum源4.安装mysql yum 源&#xff0c;对比前后yum源5.安装mysql服务6.查看配置文件和数据存储位置7.启动服务和查看启动服务8.登录9.配置my.cnf 1.卸载不要的环境 先检查是否有mariadb存在 ps ajx |grep mariadb如果…...

有病但合理的 ChatGPT 提示语

ChatGPT 面世一年多了&#xff0c;如何让大模型输出高质量内容&#xff0c;让提示词工程成了一门重要的学科。以下是一些有病但合理的提示词技巧&#xff0c;大部分经过论文证明&#xff0c;有效提高 ChatGPT 输出质量&#xff1a; ​1️⃣ Take a deep breath. 深呼吸 ✨ 作用…...

this.$emit(‘update:isVisible‘, false)作用

这个写是不是很新颖&#xff0c;传父组件传值&#xff01;这是什么鬼。。。 假设你有以下逻辑业务。在A页面弹出一个组件B&#xff0c;A组件里面使用B组件&#xff0c;是否展示B组件你使用的是baselineShow变量控制&#xff01; <BaselineData :isVisible.sync"basel…...

CnetSDK .NET OCR Library SDK Crack

CnetSDK .NET OCR Library SDK Crack CnetSDK .NET OCR Library SDK 是一款高精度 .NET OCR 扫描仪软件&#xff0c;用于从图像中识别字符&#xff0c;如文本、手写和符号。该.NET OCR库软件采用Tesseract OCR引擎技术&#xff0c;将字符识别准确率提高高达99%。通过将 .NET OC…...

基于Solr的全文检索系统的实现与应用

文章目录 一、概念1、什么是Solr2、与Lucene的比较区别1&#xff09;Lucene2&#xff09;Solr 二、Solr的安装与配置1、Solr的下载2、Solr的文件夹结构3、运行环境4、Solr整合tomcat1&#xff09;Solr Home与SolrCore2&#xff09;整合步骤 5、Solr管理后台1&#xff09;Dashbo…...