RocketMQ源码阅读-八-定时消息和消息重试

RocketMQ源码阅读-八-定时消息和消息重试

  • 定时消息
    • 概念
    • 逻辑流程图
    • 延迟级别
    • Producer发送定时消息
    • Broker存储定时消息
    • Broker发送定时消息
    • Broker 持久化定时发送进度
  • 消息重试
  • 总结

定时消息

概念

官网给出的概念:https://rocketmq.apache.org/zh/docs/featureBehavior/02delaymessage

定时消息是 Apache RocketMQ 提供的一种高级消息类型,消息被发送至Broker服务端后,在指定时间后才能被消费者消费。通过设置一定的定时时间可以实现分布式场景的延时调度触发效果。

逻辑流程图

来源https://www.iocoder.cn/RocketMQ/message-schedule-and-retry/?github&1601
image.png

延迟级别

RocketMQ 目前只支持固定精度的定时消息。

官方给出不能任意时间延迟的原因:如果要支持任意的时间精度,在 Broker 层面,必须要做消息排序,如果再涉及到持久化,那么消息排序要不可避免的产生巨大性能开销。

延迟级别相关源码如下MessageStoreConfig:

/**
 * 消息延迟级别字符串配置
 */
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

可以看到一共有18个延时级别。
解析延迟级别的代码在ScheduleMessageService:

private final ConcurrentHashMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable = new ConcurrentHashMap<>(32);
/**
 * 解析延迟级别
 *
 * @return 是否解析成功
 */
public boolean parseDelayLevel() {
    HashMap<String, Long> timeUnitTable = new HashMap<>();
    timeUnitTable.put("s", 1000L);
    timeUnitTable.put("m", 1000L * 60);
    timeUnitTable.put("h", 1000L * 60 * 60);
    timeUnitTable.put("d", 1000L * 60 * 60 * 24);

    String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel();
    try {
        String[] levelArray = levelString.split(" ");
        for (int i = 0; i < levelArray.length; i++) {
            String value = levelArray[i];
            String ch = value.substring(value.length() - 1);
            Long tu = timeUnitTable.get(ch);

            int level = i + 1;
            if (level > this.maxDelayLevel) {
                this.maxDelayLevel = level;
            }
            long num = Long.parseLong(value.substring(0, value.length() - 1));
            long delayTimeMillis = tu * num;
            this.delayLevelTable.put(level, delayTimeMillis);
        }
    } catch (Exception e) {
        log.error("parseDelayLevel exception", e);
        log.info("levelString String = {}", levelString);
        return false;
    }

    return true;
}

此方法,将延迟级别转换为毫秒数,存储在delayLevelTable中。

Producer发送定时消息

下面是官方给出发送定时消息的

        //定时/延时消息发送
        MessageBuilder messageBuilder = new MessageBuilderImpl();;
        //以下示例表示:延迟时间为10分钟之后的Unix时间戳。
        Long deliverTimeStamp = System.currentTimeMillis() + 10L * 60 * 1000;
        Message message = messageBuilder.setTopic("topic")
                //设置消息索引键,可根据关键字精确查找某条消息。
                .setKeys("messageKey")
                //设置消息Tag,用于消费端根据指定Tag过滤消息。
                .setTag("messageTag")
                .setDeliveryTimestamp(deliverTimeStamp)
                //消息体
                .setBody("messageBody".getBytes())
                .build();
        try {
            //发送消息,需要关注发送结果,并捕获失败等异常。
            SendReceipt sendReceipt = producer.send(message);
            System.out.println(sendReceipt.getMessageId());
        } catch (ClientException e) {
            e.printStackTrace();
        }
        //消费示例一:使用PushConsumer消费定时消息,只需要在消费监听器处理即可。
        MessageListener messageListener = new MessageListener() {
            @Override
            public ConsumeResult consume(MessageView messageView) {
                System.out.println(messageView.getDeliveryTimestamp());
                //根据消费结果返回状态。
                return ConsumeResult.SUCCESS;
            }
        };
        //消费示例二:使用SimpleConsumer消费定时消息,主动获取消息进行消费处理并提交消费结果。
        List<MessageView> messageViewList = null;
        try {
            messageViewList = simpleConsumer.receive(10, Duration.ofSeconds(30));
            messageViewList.forEach(messageView -> {
                System.out.println(messageView);
                //消费处理完成后,需要主动调用ACK提交消费结果。
                try {
                    simpleConsumer.ack(messageView);
                } catch (ClientException e) {
                    e.printStackTrace();
                }
            });
        } catch (ClientException e) {
            //如果遇到系统流控等原因造成拉取失败,需要重新发起获取消息请求。
            e.printStackTrace();
        }

主要通过setDeliveryTimestamp方法,设置定时时间。

Broker存储定时消息

Broker 存储消息时,延迟消息进入特定 Topic 为 SCHEDULE_TOPIC_XXXX。同时会将 延迟级别 与 消息队列编号 做固定映射:QueueId = DelayLevel - 1。
核心代码在CommitLog#putMessage中:

/**
 * 添加消息,返回消息结果
 *
 * @param msg 消息
 * @return 结果
 */
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    // ...省略代码

    // 定时消息处理
    final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE//
        || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
        // Delay Delivery
        if (msg.getDelayTimeLevel() > 0) {
            if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
            }

            // 存储消息时,延迟消息进入 `Topic` 为 `SCHEDULE_TOPIC_XXXX` 。
            topic = ScheduleMessageService.SCHEDULE_TOPIC;

            // 延迟级别 与 消息队列编号 做固定映射
            queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

            // Backup real topic, queueId
            MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
            MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
            msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));

            msg.setTopic(topic);
            msg.setQueueId(queueId);
        }
    }
    // ...省略代码
}

延迟级别 与 消息队列编号 做固定映射的代码为ScheduleMessageService#delayLevel2QueueId:

/**
 * 根据 延迟级别 计算 消息队列编号
 * QueueId = DelayLevel - 1
 *
 * @param delayLevel 延迟级别
 * @return 消息队列编号
 */
public static int delayLevel2QueueId(final int delayLevel) {
    return delayLevel - 1;
}

在生成ConsumeQueue时,每条消息的 tagsCode 使用【消息计划消费时间】。这样,ScheduleMessageService 在轮询 ConsumeQueue 时,可以使用 tagsCode 进行过滤。
相应的代码如下:

public DispatchRequest checkMessageAndReturnSize(ByteBuffer byteBuffer, final boolean checkCRC, final boolean readBody) {
    try {
        // ... 省略代码

        // 17 properties
        short propertiesLength = byteBuffer.getShort();
        if (propertiesLength > 0) {
            byteBuffer.get(bytesContent, 0, propertiesLength);
            String properties = new String(bytesContent, 0, propertiesLength, MessageDecoder.CHARSET_UTF8);
            Map<String, String> propertiesMap = MessageDecoder.string2messageProperties(properties);

            keys = propertiesMap.get(MessageConst.PROPERTY_KEYS);

            uniqKey = propertiesMap.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);

            String tags = propertiesMap.get(MessageConst.PROPERTY_TAGS);
            if (tags != null && tags.length() > 0) {
                tagsCode = MessageExtBrokerInner.tagsString2tagsCode(MessageExt.parseTopicFilterType(sysFlag), tags);
            }

            // Timing message processing
            {
                String t = propertiesMap.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL);
                if (ScheduleMessageService.SCHEDULE_TOPIC.equals(topic) && t != null) {
                    int delayLevel = Integer.parseInt(t);

                    if (delayLevel > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                        delayLevel = this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel();
                    }

                    if (delayLevel > 0) {
                        tagsCode = this.defaultMessageStore.getScheduleMessageService().computeDeliverTimestamp(delayLevel,
                            storeTimestamp);
                    }
                }
            }
        }

        int readLength = calMsgLength(bodyLen, topicLen, propertiesLength);
        if (totalSize != readLength) {
            doNothingForDeadCode(reconsumeTimes);
            doNothingForDeadCode(flag);
            doNothingForDeadCode(bornTimeStamp);
            doNothingForDeadCode(byteBuffer1);
            doNothingForDeadCode(byteBuffer2);
            log.error(
                "[BUG]read total count not equals msg total size. totalSize={}, readTotalCount={}, bodyLen={}, topicLen={}, propertiesLength={}",
                totalSize, readLength, bodyLen, topicLen, propertiesLength);
            return new DispatchRequest(totalSize, false/* success */);
        }

        return new DispatchRequest(//
            topic, // 1
            queueId, // 2
            physicOffset, // 3
            totalSize, // 4
            tagsCode, // 5
            storeTimestamp, // 6
            queueOffset, // 7
            keys, // 8
            uniqKey, //9
            sysFlag, // 9
            preparedTransactionOffset// 10
        );
    } catch (Exception e) {
    }

    return new DispatchRequest(-1, false /* success */);
}

32行调用computeDeliverTimestamp方法计算计划消费时间:

/**
 * 计算 投递时间【计划消费时间】
 *
 * @param delayLevel 延迟级别
 * @param storeTimestamp 存储时间
 * @return 投递时间【计划消费时间】
 */
public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) {
    Long time = this.delayLevelTable.get(delayLevel);
    if (time != null) {
        return time + storeTimestamp;
    }

    return storeTimestamp + 1000;
}

计算出来的计划消费时间,作为tagsCode。后面Broker发送定时消息时会用到这个tagsCode进行过滤。

Broker发送定时消息

针对延时消息队列,即每一个SCHEDULE_TOPIC_XXXX主题,每个消费队列都会有一个单独的定时任务进行轮询,用来发送到达定时的计划消费时间的消息。
流程图如下:出处;https://www.iocoder.cn/RocketMQ/message-schedule-and-retry/?github&1601
image.png
相应的实现源码在DeliverDelayedMessageTimerTask 中:
image.png
该类继承TimerTask,是一个定时任务,源码如下:

/**
 * 发送(投递)延迟消息定时任务
 */
class DeliverDelayedMessageTimerTask extends TimerTask {
    /**
     * 延迟级别
     */
    private final int delayLevel;
    /**
     * 位置
     */
    private final long offset;

    public DeliverDelayedMessageTimerTask(int delayLevel, long offset) {
        this.delayLevel = delayLevel;
        this.offset = offset;
    }

    @Override
    public void run() {
        try {
            this.executeOnTimeup();
        } catch (Exception e) {
            // XXX: warn and notify me
            log.error("ScheduleMessageService, executeOnTimeup exception", e);
            ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
                this.delayLevel, this.offset), DELAY_FOR_A_PERIOD);
        }
    }

    /**
     * 纠正可投递时间。
     * 因为发送级别对应的发送间隔可以调整,如果超过当前间隔,则修正成当前配置,避免后面的消息无法发送。
     *
     * @param now 当前时间
     * @param deliverTimestamp 投递时间
     * @return 纠正结果
     */
    private long correctDeliverTimestamp(final long now, final long deliverTimestamp) {
        long result = deliverTimestamp;

        long maxTimestamp = now + ScheduleMessageService.this.delayLevelTable.get(this.delayLevel);
        if (deliverTimestamp > maxTimestamp) {
            result = now;
        }

        return result;
    }

    public void executeOnTimeup() {
        ConsumeQueue cq = ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,  delayLevel2QueueId(delayLevel));

        long failScheduleOffset = offset;

        if (cq != null) {
            SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
            if (bufferCQ != null) {
                try {
                    long nextOffset = offset;
                    int i = 0;
                    for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                        long offsetPy = bufferCQ.getByteBuffer().getLong();
                        int sizePy = bufferCQ.getByteBuffer().getInt();
                        long tagsCode = bufferCQ.getByteBuffer().getLong();

                        long now = System.currentTimeMillis();
                        long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);

                        nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

                        long countdown = deliverTimestamp - now;

                        if (countdown <= 0) { // 消息到达可发送时间
                            MessageExt msgExt = ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
                            if (msgExt != null) {
                                try {
                                    // 发送消息
                                    MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
                                    PutMessageResult putMessageResult = ScheduleMessageService.this.defaultMessageStore.putMessage(msgInner);
                                    if (putMessageResult != null && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { // 发送成功
                                        continue;
                                    } else { // 发送失败
                                        // XXX: warn and notify me
                                        log.error("ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}", msgExt.getTopic(), msgExt.getMsgId());

                                        // 安排下一次任务
                                        ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset), DELAY_FOR_A_PERIOD);

                                        // 更新进度
                                        ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                                        return;
                                    }
                                } catch (Exception e) {
                                    // XXX: warn and notify me
                                    log.error("ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
                                            + msgExt + ", nextOffset=" + nextOffset + ",offsetPy=" + offsetPy + ",sizePy=" + sizePy, e);
                                }
                            }
                        } else {
                            // 安排下一次任务
                            ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset), countdown);

                            // 更新进度
                            ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                            return;
                        }
                    } // end of for

                    nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

                    // 安排下一次任务
                    ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);

                    // 更新进度
                    ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                    return;
                } finally {
                    bufferCQ.release();
                }
            } // end of if (bufferCQ != null)
            else { // 消费队列已经被删除部分,跳转到最小的消费进度
                long cqMinOffset = cq.getMinOffsetInQueue();
                if (offset < cqMinOffset) {
                    failScheduleOffset = cqMinOffset;
                    log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
                        + cqMinOffset + ", queueId=" + cq.getQueueId());
                }
            }
        } // end of if (cq != null)

        ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel, failScheduleOffset), DELAY_FOR_A_WHILE);
    }

    /**
     * 设置消息内容
     *
     * @param msgExt 消息
     * @return 消息
     */
    private MessageExtBrokerInner messageTimeup(MessageExt msgExt) {
        MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
        msgInner.setBody(msgExt.getBody());
        msgInner.setFlag(msgExt.getFlag());
        MessageAccessor.setProperties(msgInner, msgExt.getProperties());

        TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msgInner.getSysFlag());
        long tagsCodeValue =
            MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags());
        msgInner.setTagsCode(tagsCodeValue);
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));

        msgInner.setSysFlag(msgExt.getSysFlag());
        msgInner.setBornTimestamp(msgExt.getBornTimestamp());
        msgInner.setBornHost(msgExt.getBornHost());
        msgInner.setStoreHost(msgExt.getStoreHost());
        msgInner.setReconsumeTimes(msgExt.getReconsumeTimes());

        msgInner.setWaitStoreMsgOK(false);
        MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_DELAY_TIME_LEVEL);

        msgInner.setTopic(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC));

        String queueIdStr = msgInner.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID);
        int queueId = Integer.parseInt(queueIdStr);
        msgInner.setQueueId(queueId);

        return msgInner;
    }
}

上面代码,实现了逻辑如下:

  1. 轮询延迟消息的topic,看是否有到期的定时任务
  2. 到期的定时任务,提交到CommitLog,供消费者消费

Broker 持久化定时发送进度

  • 定时消息发送进度存储在文件(…/config/delayOffset.json)里
  • 每 10s 定时持久化发送进度

核心代码在类ScheduleMessageService中:

public void start() {
    // 定时发送消息
    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) {
            this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
        }
    }

    // 定时持久化发送进度
    this.timer.scheduleAtFixedRate(new TimerTask() {

        @Override
        public void run() {
            try {
                ScheduleMessageService.this.persist();
            } catch (Exception e) {
                log.error("scheduleAtFixedRate flush exception", e);
            }
        }
    }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}

此方法同样是启动一个定时任务,每10s执行一次持久化操作。

消息重试

消息重试发生在Consumer消费消费时,消费失败的消息会发回到Broker,进入延时消息队列,过一段时间重新消费。
所以消息重试,和定时/延时消息是密切相关的。
消费者将消费失败的消息发回Broker的源码在SendMessageProcessor#consumerSendMsgBack:

/**
 * 消费者发回消息
 *
 * @param ctx ctx
 * @param request 请求
 * @return 响应
 * @throws RemotingCommandException 当远程调用异常
 */
private RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request)
throws RemotingCommandException {
    // ... 省略部分代码
    // 处理 delayLevel
    int delayLevel = requestHeader.getDelayLevel();
    int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
    if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
        maxReconsumeTimes = requestHeader.getMaxReconsumeTimes();
    }
    if (msgExt.getReconsumeTimes() >= maxReconsumeTimes//
        || delayLevel < 0) { // 如果超过最大消费次数,则topic修改成"%DLQ%" + 分组名,即加入 死信队列(Dead Letter Queue)
        // 此时不会进入
    } else {
        if (0 == delayLevel) {
            delayLevel = 3 + msgExt.getReconsumeTimes();
        }
        // 设置延时
        msgExt.setDelayTimeLevel(delayLevel);
    }
    // ... 省略部分代码
    return response;
}

重点在于第26行,设置了延时时间。

总结

本篇分析了RocketMQ的定时消息的处理逻辑。

  • RocketMQ不支持任意时间的延迟,只支持固定时间,因为性能考虑
  • Producer发送定时消息只是调用setDeliveryTimestamp指定延迟时间或等级
  • Broker会先将定时消息,存储在特定的Topic,名字格式为 SCHEDULE_TOPIC_XXXX
  • Broker会启动一个定时任务,每1000ms执行一次,轮询 SCHEDULE_TOPIC_XXXX 中的消息,通过tagsCode过滤,将到期的消息发送到CommitLog
  • Broker同时会启动持久化定时发送进度的任务,每10s执行一次
  • 消息发送存储到Commitlog后,Consumer就可以消费到
  • 消息消费失败时,Consumer会将消息发回到Broker的延时消息Topic,固定时间后再次重试消费

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

记录威纶通:HMI

目录 HMI 界面切换​编辑 间接窗口 HMI 界面切换 第一种&#xff1a;选择功能键-切换基本窗口-操作界面-每一个界面都创建切换按钮 第二种&#xff1a;创建一个模板界面-使用功能键按照上面的操作创建两个切换按钮 选择操作界面-点击空白出右击属性-窗口设置底层选择模板界面…

flutter 五点一点四:MaterialApp Theme 给你一堆颜色看看

ColorScheme colorScheme, // 拥有30种颜色(这个数可能过几个版本会变化吧)&#xff0c;可用于配置大多数组件的颜色。 A set of 30 colors based on the[Material spec] that can be used to configure the color properties of most components.Color canvasColor, // Mater…

LeetCode 热题 100 | 子串

目录 1 560. 和为 K 的子数组 2 239. 滑动窗口最大值 3 76. 最小覆盖子串 菜鸟做题第二周&#xff0c;语言是 C 1 560. 和为 K 的子数组 题眼&#xff1a;“子数组是数组中元素的连续非空序列。” 解决本问题的关键就在于如何翻译问题。子数组 s 的和可以看作数组 i 的…

精品基于Uniapp+ssm基于java的赈灾系统App救灾救助捐赠

《[含文档PPT源码等]精品基于Uniappssm基于java的赈灾系统App》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;Java 后台框架&#xff1a;ssm 安卓框架&#xff…

防御第三次作业-防火墙组网实验(3)

目录 实验拓扑图 要求 1 2 针对10.0.2.10设备的安全策略&#xff1a; 针对10.0.2.20设备的安全策略&#xff1a; 3 4 实验拓扑图 各设备ip和接口已配好&#xff0c;均可可ping通防火墙。 要求 1.生产区在工作时间内可以访问dmz区域&#xff0c;仅可以访问http服…

通俗易懂理解FCN全卷积网络模型

温故而知新&#xff0c;可以为师矣&#xff01; 一、参考资料 深度学习笔记&#xff08;二十三&#xff09;Semantic Segmentation(FCN/U-Net/PSPNet/SegNet/U-Net/ICNet/DFANet/Fast-SCNN) 二、FCN相关介绍 1. FCN简介 FCN&#xff08;Fully Convolutional Networks&…

ELK之Grafana添加钉钉告警信息

Grafana版本如下&#xff1a; [roottest data]# grafana-server -v Version 8.4.6 (commit: c53173ff6, branch: HEAD)一、新建钉钉群&#xff0c;并自定义一个机器人 点击右上角设置 ------》 智能群助手 ------》 添加机器人 ------》右侧设置按钮 ------》点击自定义&…

如何快速搭建springboot+前后端分离(vue),多商户客户端实现微信小程序+ios+app使用uniapp(一处编写,处处编译)

kxmalls外卖生鲜多商户&#xff0c;针对中小商户、企业和个人学习者开发。使用Java编码&#xff0c;采用SpringBoot、Mybatis-Plus等易用框架&#xff0c;适合个人学习研究。同时支持单机部署、集群部署&#xff0c;用户与店铺范围动态定位&#xff0c;中小商户企业可根据业务动…

【数据结构与算法】之字符串系列-20240126

这里写目录标题 一、12. 整数转罗马数字二、43. 字符串相乘三、49. 字母异位词分组四、151. 反转字符串中的单词五、179. 最大数 一、12. 整数转罗马数字 中等 罗马数字包含以下七种字符&#xff1a; I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D …

06.Elasticsearch应用(六)

Elasticsearch应用&#xff08;六&#xff09; 1.什么是分词器 ES文档的数据拆分成一个个有完整含义的关键词&#xff0c;并将关键词与文档对应&#xff0c;这样就可以通过关键词查询文档。要想正确的分词&#xff0c;需要选择合适的分词器 2.ES中的默认分词器 fingerprint…

OpenCV笔记之图像处理中遮罩和掩模的关系

OpenCV笔记之图像处理中遮罩和掩模的关系 code review 文章目录 OpenCV笔记之图像处理中遮罩和掩模的关系1.遮罩详解遮罩的创建遮罩的应用遮罩的主要应用遮罩的类型如何创建遮罩遮罩在图像处理中的应用方式 2.遮罩和掩模的关系 1.遮罩详解 在图像处理中&#xff0c;遮罩&#…

Linux笔记之bash脚本中的-e、和

Linux笔记之bash脚本中的-e、&和&& code review! 文章目录 Linux笔记之bash脚本中的-e、&和&&1.&和&&2.-e 1.&和&& 在Linux bash脚本中&#xff0c;&符号有几个不同的用途&#xff0c;这里列举了一些常见的情况&#xf…

文件IO讲解

&#x1f495;"跑起来就有意义"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;文件IO讲解 一.与文件相关的基本概念 1.什么是文件 文件从广义上来说就是操作系统对其所持有的硬件设备和软件资源的抽象化表示,但是在日常生活中我们所提到的文件就…

三、Kotlin 类型初步

1. 类 & 接口 1.1 类的定义 1.1.1 空类的定义 Java 的定义&#xff1a; public class Foo {}Kotlin 的定义&#xff1a; class Foo注意&#xff1a; 类的访问权限修饰符默认为 public。 若类的 {} 为空&#xff0c;可以省略不写。 1.1.2 带成员的类的定义 Java 中定…

普通人如何打造自己人生的护城河?

哈喽&#xff0c;大家好啊&#xff0c;我是雷工。 今天在看《张一鸣管理日志》时看到这么一句话&#xff1a; 今日头条不断吸引更优秀的工程师&#xff0c;不断更新算法&#xff0c;才有了当前今日头条的算法护城河。 头条的算法有多牛&#xff0c;看你周边就知道了。 越来越多…

《剑指 Offer》专项突破版 - 面试题 28 : 展平多级双向链表(C++ 实现)

题目连接&#xff1a;LCR 028. 扁平化多级双向链表 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 在一个多级双向链表中&#xff0c;节点除了有两个指针分别指向前后两个节点&#xff0c;还有一个指针指向它的子链表&#xff0c;并且子链表也是一个双向链表&…

CentOS7自动备份数据库到git

虽然数据库没什么数据&#xff0c;但是有就是珍贵的啦&#xff0c;为了服务器什么的无了&#xff0c;所以还是要自动备份一下比较好。 Open备忘第一页 步骤 在Gitee&#xff08;github&#xff09;上创建一个私有仓库Gitee&#xff08;github&#xff09;配置好服务器的ssh在服…

获取双异步返回值时,如何保证主线程不阻塞?

目录 一、前情提要二、JDK8的CompletableFuture1、ForkJoinPool2、从ForkJoinPool和ThreadPoolExecutor探索CompletableFuture和Future的区别 三、通过CompletableFuture优化 “通过Future获取异步返回值”1、通过Future获取异步返回值关键代码&#xff08;1&#xff09;将异步…

k8s的图形化工具--rancher

什么是rancher&#xff1f; rancher是一个开源的企业级多集群的k8s管理平台 rancher和k8s的区别 都是为了容器的调度和编排系统&#xff0c;但是rancher不仅能够调度&#xff0c;还能管理k8s集群&#xff0c;自带监控&#xff08;普罗米修斯&#xff09; 实验部署 实验架构…

大语言模型推理提速:TensorRT-LLM 高性能推理实践

作者&#xff1a;顾静 TensorRT-LLM 如何提升 LLM 模型推理效率 大型语言模型&#xff08;Large language models,LLM&#xff09;是基于大量数据进行预训练的超大型深度学习模型。底层转换器是一组神经网络&#xff0c;这些神经网络由具有 self-attention 的编码器和解码器组…