RocketMQ5.0--定时消息

news2024/12/24 9:32:19

RocketMQ5.0–定时消息

一、定时消息概览
定时消息或延迟消息是指消息发送到Broker后,并不立即被消费而是要等到特定的时间后才能被消费。RocketMQ并不支持任意的时间精度延迟,只支持特定延迟时间的延迟消息。

消息延迟级别在Broker端通过MessageStoreConfig#messageDelayLevel配置,默认为"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h",其delayLevel=1则延迟1s,delayLevel=2则延迟5s,以此类推。解析该配置后,存放到org.apache.rocketmq.broker.schedule.ScheduleMessageService#delayLevelTable表中,格式为{1:1000,2:5000}。RocketMQ为每种延迟级别创建定时任务,这也是RocketMQ不支持任意时间延迟的原因。

注意,消息消费失败后,若是有延迟,也是和定时消息具有相同的逻辑。参考消息消费《RocketMQ5.0.0消息消费<三> _ 消息消费》。

org.apache.rocketmq.broker.schedule.ScheduleMessageService是定时消息实现类。消息存入commitlog文件之前需要判断消息的重试次数,如果大于0,则消息主题设置为SCHEDULE_TOPIC_XXXX,即:TopicValidator#RMQ_SYS_SCHEDULE_TOPIC属性。如下所示是该类的关键属性。

ScheduleMessageService方法的调用顺序:构造方法 -> load() -> start()方法。

// 第一次调度时延迟时间,默认1s
private static final long FIRST_DELAY_TIME = 1000L;
// 每一延迟级别调度一次后,则延迟该时间100ms再放入调度池
private static final long DELAY_FOR_A_WHILE = 100L;
// 发送异常后,则延迟该时间10s再放入调度池
private static final long DELAY_FOR_A_PERIOD = 10000L;
// 关闭时,等待5s
private static final long WAIT_FOR_SHUTDOWN = 5000L;
// 延迟睡10s
private static final long DELAY_FOR_A_SLEEP = 10L;
// 延迟级别表,解析MessageStoreConfig#messageDelayLevel后的数据结构{1:1000,2:5000}
private final ConcurrentMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable = new ConcurrentHashMap<Integer, Long>(32);
// 延迟级别的消息消费进度,存储在{ROCKET_HOME}/store/config/delayOffset.json
private final ConcurrentMap<Integer /* level */, Long/* offset */> offsetTable = new ConcurrentHashMap<Integer, Long>(32);
// 最大消息延迟级别
private int maxDelayLevel;
// 是否异步传送到调度池,默认关闭
private boolean enableAsyncDeliver = false;

下图所示,是定时消息实现流程图,步骤如下:

  • step1:消息存入commitlog文件之前,如果发送消息的delayLevel大于0,则改变消息主题为SCHEDULE_TOPIC_XXXX,消息队列ID为delayLevel-1;
  • step2:消息经由commitlog异步转发到主题为SCHEDULE_TOPIC_XXXX,delayLevel - 1的消息消费队列;
  • step3:定时任务Time每隔1s根据上次拉取偏移量从消费队列中取出所有消息;
  • step4:根据消息的物理偏移量与消息大小从CommitLog中拉取消息;
  • step5:根据消息属性重新创建消息,并恢复原始主题、原始消费队列,清除delayLevel属性,再存入commitlog文件;
  • step6:转发到原始主题、原始消费队列,供消费者消费。

需要注意的是延迟级别delayLevel与延迟消费队列的映射关系:消费队列ID = 延迟级别 - 1。
在这里插入图片描述
二、定时消息实现机制

1. 提交消息前的处理
消息存储流程参考《RocketMQ5.0.0消息存储<二>_消息存储流程》,其中DefaultMessageStore#asyncPutMessage执行异步存储消息时,执行存储消息钩子列表,如下代码所示。

// 异步存放消息,继续处理下一个请求;存储完成后,异步通知客户端
@Override
public CompletableFuture<PutMessageResult> asyncPutMessage(MessageExtBrokerInner msg) {
    ......
 
    // 遍历存储消息钩子列表
    for (PutMessageHook putMessageHook : putMessageHookList) {
        /*
            存储消息前验证消息格式规范;定时消息时修改为定时主题等
            如:Broker停止工作、从Broker、是否写权限、主题太长、消息体太长等
         */
        PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(msg);
        // 为null时,则消息符合规范
        if (handleResult != null) {
            return CompletableFuture.completedFuture(handleResult);
        }
    }
    
    ......
}

org.apache.rocketmq.broker.util.HookUtils#handleScheduleMessage是commit操作前对定时消息处理的核心逻辑,如下代码所示。注意事项:

  • 事务消息不能有延迟级别,若是延迟级别 > 0时,则修改为SCHEDULE_TOPIC_XXXX,消费队列ID为delayLevel -
    1;原始主题及消费队列存储到扩展属性中。
/**
 * 消息存储前,处理定时消息
 * 方法入口:{@link DefaultMessageStore#asyncPutMessage(MessageExtBrokerInner)}
 * step1:非事务消息时,检查定时消息
 * step2:若是延迟级别 > 0时,则修改为SCHEDULE_TOPIC_XXXX,消费队列ID为delayLevel - 1;原始主题及消费队列存储到扩展属性中
 *        {@link HookUtils#transformDelayLevelMessage(BrokerController, MessageExtBrokerInner)}
 * @param brokerController
 * @param msg
 * @return
 */
public static PutMessageResult handleScheduleMessage(BrokerController brokerController,
                                                     final MessageExtBrokerInner msg) {
    final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    // 非事务消息时
    if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
            || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
        if (!isRolledTimerMessage(msg)) {
            // 检查定时消息
            if (checkIfTimerMessage(msg)) {
                if (!brokerController.getMessageStoreConfig().isTimerWheelEnable()) {
                    //wheel timer is not enabled, reject the message
                    return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_NOT_ENABLE, null);
                }
                PutMessageResult tranformRes = transformTimerMessage(brokerController, msg);
                if (null != tranformRes) {
                    return tranformRes;
                }
            }
        }
        // Delay Delivery,若是延迟级别 > 0时,则修改为SCHEDULE_TOPIC_XXXX,消费队列ID为delayLevel - 1
        if (msg.getDelayTimeLevel() > 0) {
            // 修改主题及消费队列ID
            transformDelayLevelMessage(brokerController,msg);
        }
    }
    return null;
}

定时消息commit操作后(消息提交到Commitlog文件内存映射),异步转发到延迟级别对应的消费队列中,下面介绍定时任务处理延迟消费队列。

2. 定时调度

1):load()方法

org.apache.rocketmq.broker.schedule.ScheduleMessageService#load完成延迟消费进度的加载且解析延迟级别字符串,如下代码所示。注意事项:

  • 加载消费进度:加载延迟消费队列的消费进度文件,{ROCKET_HOME}/store/config/delayOffset.json文件,其格式:延迟级别:消费进度,如下实例:
{
	"dataVersion":{
		"counter":19,
		"stateVersion":0,
		"timestamp":1676598354088
	},
	"offsetTable":{3:17,12:0
	}
}
  • 解析配置:字符串MessageStoreConfig.messageDelayLevel转列表ScheduleMessageService#delayLevelTable。
/**
 * step1:加载延迟级别的消息消费进度,{ROCKET_HOME}/store/config/delayOffset.json文件
 * step2:解析MessageStoreConfig.messageDelayLevel转换为{@link ScheduleMessageService#delayLevelTable}
 * step3:矫正延迟级别消费的偏移量
 */
@Override
public boolean load() {
    // 加载延迟级别的消息消费进度文件
    boolean result = super.load();
    // 解析延迟级别
    result = result && this.parseDelayLevel();
    // 矫正延迟级别消费的偏移量
    result = result && this.correctDelayOffset();
    return result;
}

2):start()方法

org.apache.rocketmq.broker.schedule.ScheduleMessageService#start启动调度池,为每个延迟级别创建定时任务,注意事项:

  • 延迟级别delayLevel与延迟消费队列的映射关系:消费队列ID = 延迟级别 - 1
  • 创建定时任务DeliverDelayedMessageTimerTask线程:遍历延迟级别,并获取对应延迟队列的消费进度,创建定时任务。定时任务第一次启动时,默认延迟1s执行,第二次开始执行延迟级别对应的延迟时间。
  • 每10s执行调度池任务持久化延迟队列的消费进度。
/**
 * 启动调度池,为每个延迟级别创建定时任务
 * step1:加载延迟消息消费进度;
 * step2:遍历延迟级别,并获取对应的消费进度;
 * step3:创建定时任务,并放入调度池(延迟级别与延迟消费队列的映射关系:消费队列ID = 延迟级别 - 1)
 * step4:每10s执行调度池任务持久化延迟队列的消费进度(MessageStoreConfig.flushDelayOffsetInterval配置)
 */
public void start() {
    if (started.compareAndSet(false, true)) {
        // 加载延迟消息消费进度
        this.load();
        //
        this.deliverExecutorService = new ScheduledThreadPoolExecutor(this.maxDelayLevel, new ThreadFactoryImpl("ScheduleMessageTimerThread_"));
        if (this.enableAsyncDeliver) {
            this.handleExecutorService = new ScheduledThreadPoolExecutor(this.maxDelayLevel, new ThreadFactoryImpl("ScheduleMessageExecutorHandleThread_"));
        }
        // 遍历延迟级别
        for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
            Integer level = entry.getKey();
            Long timeDelay = entry.getValue();
            // 获取延迟级别对应的消费进度
            Long offset = this.offsetTable.get(level);
            if (null == offset) {
                offset = 0L;
            }
 
            if (timeDelay != null) {
                // 是否异步传送到调度池,默认关闭
                if (this.enableAsyncDeliver) {
                    this.handleExecutorService.schedule(new HandlePutResultTask(level), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
                }
                /*
                    创建Timer定时任务,并放入调度池
                    a. 定时任务第一次启动时,默认延迟1s执行,第二次开始执行对应的延迟时间
                    b. 延迟级别与延迟消费队列的映射关系:消费队列ID = 延迟级别 - 1
                 */
                this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
            }
        }
 
        // 每10s执行持久化延迟队列的消费进度(MessageStoreConfig.flushDelayOffsetInterval配置)
        this.deliverExecutorService.scheduleAtFixedRate(new Runnable() {
 
            @Override
            public void run() {
                try {
                    if (started.get()) {
                        // 持久化延迟队列的消费进度
                        ScheduleMessageService.this.persist();
                    }
                } catch (Throwable e) {
                    log.error("scheduleAtFixedRate flush exception", e);
                }
            }
        }, 10000, this.brokerController.getMessageStore().getMessageStoreConfig().getFlushDelayOffsetInterval(), TimeUnit.MILLISECONDS);
    }
}

3):定时调度任务
org.apache.rocketmq.broker.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask是个定时调度任务线程,其下图是该线程run()的调用链。
在这里插入图片描述
org.apache.rocketmq.broker.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask#executeOnTimeup是调用任务的核心方法,代码如下。注意事项:

  • 获取延迟消费队列:根据延迟级别(映射为延迟消费队列ID) + 延迟主题。

  • 延迟时间是否到期:延迟时间deliverTimestamp = 延迟级别对应的延迟时间 + 消息存储时间戳,根据差值来判定是否到期countdown = deliverTimestamp - now:
    countdown > 0时:说明没有到延迟时间,则;执行下一个调度任务(100ms后从currOffset偏移量开始执行定时任务) + 更新消费进度。
    countdown <= 0时,说明到延迟时间,可以消费消息。

  • 还原原始消息:根据偏移量从Commitlog获取延迟消息,后恢复到原始消息(原始topic、原始消费队列),清除延迟级别,保留消费次数。

  • 同步syncDeliver或异步asyncDeliver处理:原始消息重新提交到Commitlog中,供消费者消费。

  • scheduleNextTimerTask(long offset, long delay):offset当前消费进度,delay(默认100ms)是定时任务100ms后从当前offset再次执行调度任务。

/**
 * 定时消息调度核心方法
 * 注意:消息消费失败返回ACK时,根据delayLevel > 0时,改变消息主题为SCHEDULE_TOPIC_XXXX,延迟消费队列ID = delayLevel -1
 *      或
 *      延迟消息(topic为:SCHEDULE_TOPIC_XXXX)写入Commitlog,进而转发到延迟消息队列(延迟消费队列ID = delayLevel -1)
 * step1:根据延迟级别(映射为消费队列ID) + 延迟主题 获取 延迟消费队列
 * step2:获取当前消费进度后的所有消息{@link ConsumeQueueInterface#iterateFrom(long)}
 * step3:遍历消息,获取消息的偏移量、大小、Tag哈希码,为获取Commitlog完整消息准备
 * step4:判断消息TAG的哈希码是否有效,计算当前延迟时间 = 延迟级别对应的延迟时间 + 消息存储时间戳
 *        {@link ScheduleMessageService#computeDeliverTimestamp(int, long)}
 * step5:矫正延迟时间{@link DeliverDelayedMessageTimerTask#correctDeliverTimestamp(long, long)}
 * step6:判定延迟是否到期:countdown = deliverTimestamp - now
 *        countdown > 0时:说明没有到延迟时间,则;执行下一个调度任务(100ms后从currOffset偏移量开始执行定时任务) + 更新消费进度
 *        countdown <= 0时,说明到延迟时间,可以消费消息
 * step7:根据延迟消息的偏移量、大小从Commitlog获取完整延迟消息
 *        {@link MessageStore#lookMessageByOffset(long, int)}
 * step8:延迟消息恢复到原始消息(原始topic、原始消费队列),清除延迟级别,保留消费次数
 *        {@link ScheduleMessageService#messageTimeup}
 * step9:原始消息再次放入Commitlog,并转发到相应的原始消费队列,供消费者消费
 *        {@link DeliverDelayedMessageTimerTask#asyncDeliver} 和 {@link DeliverDelayedMessageTimerTask#syncDeliver}
 */
public void executeOnTimeup() {
    // 根据延迟级别(映射为消费队列ID) + 延迟主题 获取 延迟消费队列
    ConsumeQueueInterface cq =
        ScheduleMessageService.this.brokerController.getMessageStore().getConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
            delayLevel2QueueId(delayLevel));
 
    // 消费队列为null,说明没有该延迟级别的消费队列,忽略本次调度任务,创建下次调度任务
    if (cq == null) {
        // 下次调度任务
        this.scheduleNextTimerTask(this.offset, DELAY_FOR_A_WHILE);
        return;
    }
 
    // 获取当前消费进度后的所有消息
    ReferredIterator<CqUnit> bufferCQ = cq.iterateFrom(this.offset);
    // 未找到消息,创建下次调度任务
    if (bufferCQ == null) {
        long resetOffset;
        if ((resetOffset = cq.getMinOffsetInQueue()) > this.offset) {
            log.error("schedule CQ offset invalid. offset={}, cqMinOffset={}, queueId={}",
                this.offset, resetOffset, cq.getQueueId());
        } else if ((resetOffset = cq.getMaxOffsetInQueue()) < this.offset) {
            log.error("schedule CQ offset invalid. offset={}, cqMaxOffset={}, queueId={}",
                this.offset, resetOffset, cq.getQueueId());
        } else {
            resetOffset = this.offset;
        }
 
        this.scheduleNextTimerTask(resetOffset, DELAY_FOR_A_WHILE);
        return;
    }
 
    long nextOffset = this.offset;
    try {
        while (bufferCQ.hasNext() && isStarted()) {
            // 获取消息队列元素
            CqUnit cqUnit = bufferCQ.next();
 
            // 获取消息的偏移量、大小、Tag哈希码,为获取Commitlog完整消息准备
            long offsetPy = cqUnit.getPos();      // 消息偏移量
            int sizePy = cqUnit.getSize();        // 消息大小
            long tagsCode = cqUnit.getTagsCode(); // 消息TAG的哈希码
 
            // 消息TAG的哈希码是否有效
            if (!cqUnit.isTagsCodeValid()) {
                //can't find ext content.So re compute tags code.
                log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
                    tagsCode, offsetPy, sizePy);
                // 获取当前消息的存储时间戳
                long msgStoreTime = ScheduleMessageService.this.brokerController.getMessageStore().getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
                // 计算当前延迟时间 = 延迟级别对应的延迟时间 + 消息存储时间戳
                tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
            }
 
            long now = System.currentTimeMillis();
            // 矫正延迟时间
            long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
 
            // 计算下条消息的偏移量
            long currOffset = cqUnit.getQueueOffset();
            assert cqUnit.getBatchNum() == 1;
            nextOffset = currOffset + cqUnit.getBatchNum();
 
            // 定时消息的到期时间,> 0时:说明没有到延迟时间;<= 0时,说明到延迟时间,可以消费消息
            long countdown = deliverTimestamp - now;
            // > 0时:说明没有到延迟时间
            if (countdown > 0) {
                // 没有到延迟时间,执行下一个调度任务(100ms后从currOffset偏移量开始执行定时任务)
                this.scheduleNextTimerTask(currOffset, DELAY_FOR_A_WHILE);
                // 更新该延迟级别对应消费队列的消费进度
                ScheduleMessageService.this.updateOffset(this.delayLevel, currOffset);
                return;
            }
 
            // 从Commitlog获取完整消息(延迟消息)
            MessageExt msgExt = ScheduleMessageService.this.brokerController.getMessageStore().lookMessageByOffset(offsetPy, sizePy);
            if (msgExt == null) {
                continue;
            }
 
            /*
             * 从Commitlog获取的延迟消息转为之前的原始消息
             * 清除延迟级别属性;恢复原先的消息topic、消费队列;消费次数不会丢失
             */
            MessageExtBrokerInner msgInner = ScheduleMessageService.this.messageTimeup(msgExt);
            if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) {
                log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}",
                    msgInner.getTopic(), msgInner);
                continue;
            }
 
            boolean deliverSuc;
            // 异步传送到Commitlog
            if (ScheduleMessageService.this.enableAsyncDeliver) {
                deliverSuc = this.asyncDeliver(msgInner, msgExt.getMsgId(), currOffset, offsetPy, sizePy);
            }
            // 同步传送到Commitlog
            else {
                deliverSuc = this.syncDeliver(msgInner, msgExt.getMsgId(), currOffset, offsetPy, sizePy);
            }
 
            if (!deliverSuc) {
                this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
                return;
            }
        }
    } catch (Exception e) {
        log.error("ScheduleMessageService, messageTimeup execute error, offset = {}", nextOffset, e);
    } finally {
        bufferCQ.release();
    }
 
    this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
}
 
// 下次调度任务
public void scheduleNextTimerTask(long offset, long delay) {
    ScheduleMessageService.this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(
        this.delayLevel, offset), delay, TimeUnit.MILLISECONDS);
}

三、参考资料
https://blog.csdn.net/yunqiinsight/article/details/126284555

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/727385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue中控制element表格列的显示与隐藏

背景 根据‘执行进度计算方式’的单选框里面的选项不同&#xff0c;展示不同的column 按最小制剂单位统计: 按含量统计: 实现方式 就是拿到选项框里面的值&#xff0c;再根据里面的值来判断哪些column显示和隐藏&#xff1b;关于显示和隐藏可以设置变量&#xff1b; <…

SpringBoot原理分析 | Spring Data整合:JDBC、Druid、Mybatis

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Spring Data Spring Data是一个用于简化数据库访问和操作的开源框架&#xff0c;为开发人员提供了一种通用的方式来处理不同类型的数据存储&#xff0c;例如关系型数据…

android13(T) Settings 主页面 Suggestion 菜单源码分析

1、什么是 Suggestion 菜单 呐&#xff0c;下面这个就是 Suggestion 菜单&#xff0c;一般出现在设置主界面最上方位置。 出现时机需要满足三个条件&#xff0c;1、设备不是 LowRam 设备 2、启用 settings_contextual_home 特性 3、在开机一定时间后(一般是几天&#xff0c;具…

山西电力市场日前价格预测【2023-07-08】

日前价格预测 预测明日&#xff08;2023-07-08&#xff09;山西电力市场全天平均日前电价为341.87元/MWh。其中&#xff0c;最高日前电价为871.53元/MWh&#xff0c;预计出现在22: 15。最低日前电价为143.16元/MWh&#xff0c;预计出现在13: 30。以上预测仅供学习参考&#xff…

缓存设计(本地缓存 + 分布式缓存)

缓存设计 前言正文缓存对象缓存服务缓存策略本地缓存Guava的使用 分布式缓存Redis缓存分布式缓存的生命周期分布式缓存的一致性问题 源码解读从缓存中获取秒杀品 分布式锁 总结参考链接 前言 大家好&#xff0c;我是练习两年半的Java练习生&#xff0c;本篇文章会分析秒杀系统…

el-form实现其中一个填写即可的校验

<el-formref"form":model"formData":rules"formRules"label-width"130px"><el-row :gutter"24"><el-col :span"12"><el-form-item label"司机姓名 :" prop"driverName"…

【贪心+最小子段和】EDU151 D

Problem - D - Codeforces 题意&#xff1a; 思路&#xff1a; 首先K是1e18的范围&#xff0c;不能去枚举&#xff0c;那么就去考虑猜测结论 手推样例&#xff1a; 初步可以猜测&#xff0c;K应该取的是某个峰值 结论是&#xff0c;K应该取最小子段和的左端点 因为当前缀和…

【Qt QML入门】第一个Quick应用

运行结果&#xff1a; 打开Qt Creator&#xff0c;创建一个Qt Quick Qpplication&#xff0c;IDE为我们创建一个应用工程&#xff0c;其中包含如下文件&#xff1a; .pro工程文件&#xff0c;我们通过它来打开整个工程&#xff1a; QT quick# You can make your code fail to…

这个618,项目经理竟然只能买它

早上好&#xff0c;我是老原。 转眼就来到了2023年年中&#xff0c;你们的个人成长计划启动了吗&#xff1f; 比如读书计划。 最近有不少粉丝朋友私信老原&#xff0c;希望能推荐一些可以帮自己“进化”的神作。 每个人的基础不同&#xff0c;想要“进化”还是得对症下药才…

C# --- 类型安全 与 var关键字

C# --- 类型安全 与 var关键字 什么是类型安全var关键字 什么是类型安全 类型安全就是编译器在编译阶段会检查变量内容和变量类型是否匹配, 如果不匹配会抛出错误类型安全的语言包括Java, C, C#等类型不安全的语言有JavaScript 下面这段代码是JavaScript, 编译器不会进行类型检…

git在工作中如何搭建和运用(巨详细!!)

最近有点闲&#xff0c;出一版git在实际公司上的一些运用 1&#xff0c;下载git&#xff0c; 下载git就不多说了&#xff0c;官方上下载安装就好了。 2&#xff0c;初始化 下载安装完成后&#xff0c;找个项目的空文件夹进去&#xff0c;右键点击git bash here &#xff0c;…

servlet和form和session表单实现最简单的登录跳转功能(详解,文末付源码)

目录 第一步&#xff1a;配置环境 在pom.xml引入servlet等依赖 这段代码赋值粘贴进web.xml 第二步&#xff1a;编写前端html的form表单 html代码&#xff08;复制这个&#xff09; 第三步&#xff1a;编写登录的java loginservlet代码&#xff08;复制这个&#xff09; 解释…

vue使用element plus引入ElMessage样式失效的问题

样式失效如图&#xff1a; 我使用的是按需引用&#xff0c;所以在main.js中直接导入下面样式就行&#xff1a; import element-plus/theme-chalk/index.css

Luogu P1280.尼克的任务

Luogu P1280.尼克的任务 原题点这里 思路 方法一&#xff1a;动态规划 这是一道动态规划的题目。 步骤主要分 5 5 5 步&#xff1a; 状态的定义转移式的推到递推顺序的判定边界的确定结果的输出 下面&#xff0c;我们针对这道题&#xff0c;细细地讲解一下每一个步骤 一…

MYSQL单表数据量达到多少时性能会严重下降的问题探讨!

不知从什么时候开始&#xff0c;有着MySQL单表数据量超过2000万性能急剧下降的说法。 在中国互联网技术圈流传着这么一个说法&#xff1a;MySQL 单表数据量大于 2000 万行&#xff0c;性能会明显下降。事实上&#xff0c;这个传闻据说最早起源于百度。具体情况大概是这样的&am…

PS 魔棒选区工具使用方法

我们现在PS中打开一个项目 然后 如下图 在工具左侧 选择魔棒工具 选择魔棒工具之后 我们的鼠标会变成像一个魔法棒一样的东西 我们拿着魔棒工具 在下图指向位置点一下 就可以看到 它在我们整个图上生成了一些选区 这个工具本身也带有一些色彩识别的功能 就相当于 你点的这…

【MySQL系列】MySQL库的学习及基本操作(增删查改)

「前言」文章内容大致是数据库的基本操作 「归属专栏」MySQL 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「句子分享」 哪里会有人喜欢孤独&#xff0c;不过是不喜欢失望罢了。 ——村上春树《挪威的森林》 目录 一、创建/查看数据库二、删除数据库三、…

Java8 lambda 表达式 forEach 如何提前终止?

首先&#xff0c;让我们看一下Java 8中如何使用forEach()方法。forEach()方法接受一个Consumer接口作为参数&#xff0c;该接口定义了一个accept()方法&#xff0c;该方法接受一个对象并对其执行一些操作。因此&#xff0c;我们可以通过Lambda表达式来实现Consumer接口。下面是…

Camtasia2023中文版电脑屏幕记录和课件制作工具

TechSmith Camtasia是一个非常容易使用的电脑屏幕记录和课件制作工具。Camtasia 2023软件集强大的录屏、视频编辑编辑、视频菜单制作、视频影院和视频播放功能于一体&#xff0c;可以轻松制作各种教学课件、微课堂等。Camtasia 2023支持一键录制和共享高质量截屏视频&#xff0…

Pytorch: 数据读取机制Dataloader与Dataset

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 pytorch教程 也可获取。 文章目录 数据读取机制Dataloader与DatasetDataLoader 与 Datasettorch.utils.data.DataLoader区分Epoch、Iteration、Batchs…