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() {@Overridepublic 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 Deliveryif (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, queueIdMessageAccessor.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 propertiesshort 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, // 1queueId, // 2physicOffset, // 3totalSize, // 4tagsCode, // 5storeTimestamp, // 6queueOffset, // 7keys, // 8uniqKey, //9sysFlag, // 9preparedTransactionOffset// 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;}@Overridepublic void run() {try {this.executeOnTimeup();} catch (Exception e) {// XXX: warn and notify melog.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 melog.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 melog.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 fornextOffset = 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() {@Overridepublic 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 {// ... 省略部分代码// 处理 delayLevelint 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,固定时间后再次重试消费

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

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

相关文章

树莓派无显示屏连接

终端命令控制树莓派关机 1&#xff1a;用网线连接树莓派 按照正常的步骤 &#xff0c;搜索控制面板&#xff0c;网络和internet&#xff0c;网络和共享中心&#xff0c;更改适配器设置&#xff0c;右键WIFI&#xff0c;点击属性&#xff0c;点击共享&#xff0c;打勾允许即可&…

redis排序

文章目录 简介SORT命令的实现ALPHA选项的实现ASC和DESCBYLIMITGET命令 类似映射STORE选项的实现多个命令的执行顺序 简介 Redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。 SORT命令的实现 服务器执行SORT numbers 命令的详细步骤如下&#xff1a; 1&#…

超分之ESRGAN

Esrgan&#xff1a;增强型超分辨率生成对抗网络。Esrgan: Enhanced super-resolution generative adversarial networks.In: ECCVW. (2018)Xintao Wang, Ke Yu, Shixiang Wu, Jinjin Gu, Yihao Liu,Chao Dong, Yu Qiao, and Chen Change Loy. 文章目录 摘要一、引言二、相关工作…

科大讯飞 再次引爆Ai

去年「科大讯飞版ChatGPT」星火大模型刚上线的时候&#xff0c;小编给大家推荐过一波&#xff0c;演示了其强大的功能&#xff0c;不少小伙伴都立马申请体验了一把&#xff0c;有小伙伴还私信我说功能非常强大&#xff0c;工作效率提高不少&#xff0c;支持国产大模型之类赞扬。…

西安石油大学C++上机实验汇总

考试题难度就像第三章第五章课后题的难度 基础知识一定要掌握&#xff01;&#xff01;&#xff01; 上机一&#xff1a;类与对象程序设计&#xff08;2 学时&#xff09; 上机目的 掌握声明类的方法。掌握类和类的成员的概念以及定义对象的方法。掌握构造函数和析构函数的…

elementplus 中 DatePicker 日期选择器样式修改无效

问题 遇到一个需求需要修改 DatePicker 日期选择器的样式&#xff0c;在添加了 scoped 属性的 style 标签&#xff0c;并且使用了 deep 样式穿透的情况下并不能修改其样式。 原因 DatePicker 日期选择器弹出面板默认挂载在 body 上&#xff0c;所以在组件中添加了 scoped 属…

Java日期和时间学习记录2

1、Java的LocalDate类当前时间往未来几天和过去时间几天 import java.time.LocalDate;public class Main {public static void main(String[] args) {// 获取当前日期LocalDate currentDate LocalDate.now();System.out.println("当前日期&#xff1a;" currentDa…

【Fooocus 深度学习】SDXL,AIGC生图,源码解读

文章目录 使用通配符增加prompt多样性Fooocus的风格实现 使用通配符增加prompt多样性 prompt和negative_prompt都可以通过apply_wildcards函数来实现通配符替换&#xff0c;apply_wildcards会从txt中随机找一个出来。 promptsunshine, river, trees, __artist__ task_prompt …

vue2、vue3,生命周期详解

一、Vue2.x Vue2的生命周期 是指Vue实例从创建到销毁的整个过程中&#xff0c;会经历一系列的阶段和回调函数。它分为8个阶段&#xff0c;包括了组件的创建、挂载、更新和销毁等过程。 1、beforeCreate: 在实例初始化之后&#xff0c;但在数据观测和事件配置之前被调用。此…

使用ffmpeg转换索尼老DV拍摄的VOB文件为mp4

一些背景故事 最近对象想用 CCD 拍照录像&#xff0c;家里刚好有一台快 20 年前的索尼 DV DCR-DVD653E&#xff0c;就是电池老化充不进去电了。 翻出来之后还感慨了一下&#xff1a;当年没有网购&#xff0c;价格不透明&#xff1b;有些地方也没有官方店&#xff0c;只有一两家…

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

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

js中的内置对象、数学对象、日期对象、数组对象、字符串对象

js中的对象&#xff08;三种&#xff09;&#xff1a; 自定义对象 car、computer DOM对象 div、p BOM对象 window、console 内置对象 数学对象 Math &#xff08;object类型&#xff09; 1、圆周率 Math.PI 2、向下取整(返回值) Math.floor() 3、向上取整(返回值) M…

年少不知 Base 好,错把总包当成宝。。

今天聊一个很现实的话题&#xff1a;选 offer 对比薪资时&#xff0c;我强烈建议以 Base 为主&#xff0c;而不是总包。 为什么&#xff1f;且听鱼皮娓娓道来。 注意&#xff0c;以下为个人观点&#xff0c;仅供参考&#xff01; 首先明确 Base 和总包的概念&#xff1a; Base&…

某马头条——day11+day12

实时计算和定时计算 流式计算 kafkaStream 入门案例 导入依赖 <dependency><groupId>org.apache.kafka</groupId><artifactId>kafka-streams</artifactId><exclusions><exclusion><artifactId>connect-json</artifactId&…

美,英,法,德、意大利和西班牙的geojson,以及区域json

美&#xff0c;英&#xff0c;法&#xff0c;德、意大利和西班牙的geojson文件 json地址 https://pan.baidu.com/s/1nio1bV_j-jAEVqgEHXWsNw?pwdqwer#list/path/GEOJSON 感谢大佬提供的 大佬连接 大佬的知乎原地址 国内geojson获取工具地址 http://da![在这里插入图片描述](h…

【江科大】STM32:I2C通信外设(硬件)

在将2C通信外设之前&#xff0c;我们先捋一捋&#xff0c;串口的相关特点来和I2C进行一个对北比。 首先&#xff1a; 1,大部分单片机&#xff0c;设计的PCB板均带有串口通信的引脚&#xff08;也就是通信基本都借助硬件收发器来实现&#xff09; 2.对于串口的异步时序&#xff…

韩国访问学者申请注意事项

随着国际交流的增加&#xff0c;韩国成为许多学者追求学术深造的热门目的地之一。如果你计划成为一名韩国访问学者&#xff0c;以下是知识人网小编整理的一些需要注意的事项&#xff0c;以确保你的申请顺利进行。 1.详细了解目标学府&#xff1a;在开始申请之前&#xff0c;仔细…

Python进阶——文件及IO操作

一、文件的基本操作 创建文件对象和打开文件对象&#xff1a;open() def my_write():#(1)打开&#xff08;创建&#xff09;文件fileopen(Myqq.txt,w,encodingutf-8) #如果文件不存在&#xff0c;则在当前目录下创建一个文件名字为Myqq.txt#&#xff08;2&#xff09;操作文件f…

redis整合

一.redis的发布订阅 什么 是发布和订阅 Redis 发布订阅 (pub/sub) 是一种消息通信模式&#xff1a;发送者 (pub) 发送消息&#xff0c;订阅者 (sub) 接收消息。 Redis 客户端可以订阅任意数量的频道。 1、Redis的发布和订阅 客户端订阅频道发布的消息 频道发布消息 订阅者就可以…

matlab绘图杂谈-stem函数和plot函数

出发点 今天在论文中看到一副这样的图&#xff0c;它既有曲线&#xff0c;又有点&#xff0c;并且对两者都添加了图例。三条曲线应该是用plot函数绘制的&#xff0c;而target哪个绿色的圆圈&#xff0c;我的理解是用stem函数绘制的。它只是1个点&#xff0c;并且没有竖线&…