范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

RocketMQ源码分析之NameServer核心组件RouteInfoManager源码分析

  一、前言
  前文我们介绍了NameServer核心组件KVConfigManager,本文我们介绍NameServer另一个核心组件RouteInfoManager路由数据管理组件,该组件存放着整个消息集群的相关消息;
  二、RouteInfoManager构造方法及字段public class RouteInfoManager {      private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);      // broker网络长连接过期时间,长连接空闲过期时间是2分钟     private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;      // 读写锁     private final ReadWriteLock lock = new ReentrantReadWriteLock();     // 创建topic以后,每个topic是逻辑上的概念,都是有多个queue,这些queue分散在不同的broker组里     // topic->queues     private final HashMap> topicQueueTable;     // 一个broker name -> broker data,代表的是一个broker组,一个broker data应该是包含了一组broker数据     private final HashMap brokerAddrTable;     // 一个nameserver是可以管理多个broker cluster,通常来说就一个cluster就可以了     // 多业务,对于大型的公司来说,他可能是有多个业务的,每个业务是可以部署独立的broker集群,对应的都是一个nameserver     private final HashMap> clusterAddrTable;     // 顾名思义,他应该是用于管理跟broker之间的长连接、是否还有心跳、保活     private final HashMap brokerLiveTable;     // filter server是什么东西,rocketmq高阶的功能,我们可以基于tag来进行数据筛选,比较简单,没办法支持更加复杂细粒度的数据筛选     // rocketmq是支持一个高阶功能,叫做filter server,在每台broker机器上是可以启动一个filter server     // filter server启动之后会跟本地的broker来进行长连接构建,注册,以及心跳和保活     // 我们可以把一个自定义的消息筛选的class,一个类,上传到filter server里去,我们消费数据的时候,让broker     // 把数据先传输到本地机器的filter server里去,filter server基于你自定义的class来进行细粒度的数据筛选     // 把精细筛选后的数据再回传给你的消费端     // 每个broker机器上是可以启动一个或者是多个filter server,都会传输给nameserver     private final HashMap/* Filter Server */> filterServerTable;      public RouteInfoManager() {         this.topicQueueTable = new HashMap>(1024);         this.brokerAddrTable = new HashMap(128);         this.clusterAddrTable = new HashMap>(32);         this.brokerLiveTable = new HashMap(256);         this.filterServerTable = new HashMap>(256);     } 	// 省略… }public class QueueData implements Comparable {      private String brokerName; // 每个queue都属于一个数据分区,一定是在一个broker组里     private int readQueueNums; // 分成write queue和read queue     private int writeQueueNums; // write queue是用于写入数据的路由的,read queue是用于消费数据的路由的      // 在这个broker里,我的topic有4个write queue,还有4个read queue     // 随机的从4个write queue里获取到一个queue来写入数据,在消费的时候,从4个read queue里随机的挑选一个,来读取数据     // 4个write queue,2个read queue -> 会均匀的写入到4个write queue里去,读数据的时候仅仅会读里面的2个queue的数据     // 4个write queue,8个read queue -> 你只会写入4个queue里,但是消费的时候随机从8个queue里消费的      // 区分读写队列作用是帮助我们对topic的queues进行扩容和缩容,8个write queue + 8个read queue     // 4个write queue -> 写入数据仅仅会进入这4个write queue里去     // 8个read queue,读取数据,有4个queue持续消费到最新的数据,另外4个queue不会写入新数据,但是会把他     // 也有的数据全部消费完毕,把8个read queue -> 4个read queue      private int perm;     private int topicSysFlag; }public class BrokerData implements Comparable {      // broker集群拓扑架构,一个broker集群 -> 多个broker组(broker name)-> 多个broker机器(主从复制,高可用)     // 这一组broker是属于哪个cluster     private String cluster;     // broker name代表了当前的broker组     private String brokerName;     // 当前这一组broker里面包含了具体的几个broker机器,     private HashMap brokerAddrs;      private final Random random = new Random();      public BrokerData() {      } }class BrokerLiveInfo {      // broker是可以主动给nameserver上报心跳,每次上报都可以更新这个时间戳     private long lastUpdateTimestamp;     // broker数据版本号     private DataVersion dataVersion;     // netty channel,网络连接,长连接的概念     private Channel channel;     // 跟你当前这个broker机器构成HA高可用的broker地址     private String haServerAddr;      public BrokerLiveInfo(long lastUpdateTimestamp, DataVersion dataVersion, Channel channel,         String haServerAddr) {         this.lastUpdateTimestamp = lastUpdateTimestamp;         this.dataVersion = dataVersion;         this.channel = channel;         this.haServerAddr = haServerAddr;     } }topicQueueTable:topic消息队列的路由信息,消息发送的时候会根据路由表进行负载均衡。Key为topic名称,value也是一个Map:以brokerName为key,value是队列数据如上代码所示,包含读/写队列数量、权重等。brokerAddrTable:broker的基础信息,Key为brokerName,value包含brokerName,broker所在的集群信息,主备broker的地址。clusterAddrTable:broker集群信息,Key为集群名称(clusterName),value存储的是集群中所有broker的名称(brokerName)。brokerLiveTable:Broker状态信息,NameServer每次收到心跳包时会替换该信息。这也是NameServer每10秒要扫描的信息。filterServerTable:Broker上的FilterServer列表,用于类模式消息过滤。类模式过滤机制在4.4及以后版本被废弃。三、路由注册流程分析加写锁,防止并发修改路由表。首先判断Broker所属的集群(clusterName)是否存在,如果不存在则创建集群(clusterAddrTable),然后将Broker的名称添加到集群的Broker集合中。维护BrokerData信息,先从brokerAddrTable中根据Broker的名称来获取BrokerData,如果不存在,则新建一个BrokerData并保存进brokerAddrTable,registerFirst设置为true。如果该Broker已经存在对应的BrokerData,直接替换掉原来的,registerFirst为false。registerFirst为true表示第一次注册。如果接收到的Broker信息为主节点,并且Broker的Topic配置发生了变化或者是第一次注册,则需要创建或更新Topic的路由元数据(QueueData),并且把路由元数据设置/更新到topicQueueTable。其实就是为默认主题自动注册路由信息,其中包含MixAll.DEFAULT_TOPIC的路由信息。当消息生产者发送消息到主题时,如果该主题未创建,并且BrokerConfig的autoCreateTopicEnable为true,则返回MixAll.DEFAULT_TOPIC的路由信息。更新brokerLiveTable,存储能正常使用的Broker信息。BrokerLiveInfo是执行路由删除操作的重要依据。注册Broker的过滤器Server地址列表,一个Broker会关联多个FilterServer消息过滤服务器。如果此Broker是从节点,还需要查找该Broker的主节点信息,并且更新对应的masterAdd属性。最后解锁,返回注册结果。public RegisterBrokerResult registerBroker(     final String clusterName, // broker所属的cluster集群     final String brokerAddr, // broker机器地址     final String brokerName, // broker所属的组名称     final long brokerId, // broker机器自己的id     final String haServerAddr, // 跟你的这个broker互为HA高可用的一个机器地址     final TopicConfigSerializeWrapper topicConfigWrapper, // 当前的这个broker机器上面包含的topic队列数据     final List filterServerList, // broker机器上面部署的filter server列表     final Channel channel) { // 物理上的netty channel网络长连接     RegisterBrokerResult result = new RegisterBrokerResult();     try {         try {             // 路由注册需要枷锁,防止并发修改RouteInfoManger中的路由表。             this.lock.writeLock().lockInterruptibly();              // 拿到一个cluster集群对应的broker组,把我们的这个broker组加入到cluster里去             // 为什么要用set数据结构,一个broker组是有多个broker机器,会注册多次,组加入cluster             // 必须是set,这样可以对组进行去重             Set brokerNames = this.clusterAddrTable.get(clusterName);             if (null == brokerNames) {                 brokerNames = new HashSet();                 this.clusterAddrTable.put(clusterName, brokerNames);             }             brokerNames.add(brokerName);              // broker组是不是第一次来注册             boolean registerFirst = false;              // 如果是broker组第一次来注册,给初始化一份broker组数据             BrokerData brokerData = this.brokerAddrTable.get(brokerName);             if (null == brokerData) {                 registerFirst = true;                 brokerData = new BrokerData(clusterName, brokerName, new HashMap());                 this.brokerAddrTable.put(brokerName, brokerData);             }             // 拿到broker组数据里的小map,broker组里的broker机器map             Map brokerAddrsMap = brokerData.getBrokerAddrs();             // Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>             // The same IP:PORT must only have one record in brokerAddrTable             // 这个地方是处理一些异常数据,如果说你注册过来的broker机器地址跟之前注册过的机器地址是一样的             // 但是broker id是不同的,同一台机器,你启动了不同的broker节点(用的是不同的broker.conf),是不对的             Iterator> it = brokerAddrsMap.entrySet().iterator();             while (it.hasNext()) {                 Entry item = it.next();                 if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {                     it.remove();                 }             }             // 把本次要注册broker地址放到了broker组对应的broker机器地址列表里去             String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);             registerFirst = registerFirst || (null == oldAddr);              // 如果说你是一组broker里的master,而且你上报了你管理的topic数据             // 处理broker组管理的topic的队列数据,会更新到内存的map里去             if (null != topicConfigWrapper                 && MixAll.MASTER_ID == brokerId) {                 // 如果broker是主节点并且topic配置信息发生该表(dataVersion不一致)或者是初次注册,需要创建或更新topic路由元数据                 // 并填充topicQueueTable,其实就是为默认主题自动注册路由信息,其中包含 MixAll.DEFAULT_TOPIC的路由信息。                 if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())                     || registerFirst) {                     ConcurrentMap tcTable =                         topicConfigWrapper.getTopicConfigTable();                     if (tcTable != null) {                         for (Map.Entry entry : tcTable.entrySet()) {                             // 更新或创建新的 QueueData                             this.createAndUpdateQueueData(brokerName, entry.getValue());                         }                     }                 }             }              // 维护跟broker之间的保活数据             BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,                 new BrokerLiveInfo(                     System.currentTimeMillis(),                     topicConfigWrapper.getDataVersion(),                     channel,                     haServerAddr)             );             if (null == prevBrokerLiveInfo) {                 log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);             }              // 维护broker机器上部署的filter server的列表             if (filterServerList != null) {                 if (filterServerList.isEmpty()) {                     this.filterServerTable.remove(brokerAddr);                 } else {                     this.filterServerTable.put(brokerAddr, filterServerList);                 }             }              // 如果说注册过来的机器是一组broker里的slave             if (MixAll.MASTER_ID != brokerId) {                 String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);                 if (masterAddr != null) {                     BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);                     if (brokerLiveInfo != null) {                         // 他会把你的一组broker里的slave broker来注册的时候                         // 给你的注册结果里设置进去你的ha server addr,是你的这一组broker里master他的ha server addr                         result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());                         // 他还会把你这一组的broker里的master地址设置进去返回给你                         result.setMasterAddr(masterAddr);                     }                 }             }         } finally {             this.lock.writeLock().unlock();         }     } catch (Exception e) {         log.error("registerBroker Exception", e);     }      return result; }// 维护topic在各个broker里的队列数据 private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {     // 创建队列信息     QueueData queueData = new QueueData();     queueData.setBrokerName(brokerName);     queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());     queueData.setReadQueueNums(topicConfig.getReadQueueNums());     queueData.setPerm(topicConfig.getPerm());     queueData.setTopicSysFlag(topicConfig.getTopicSysFlag());      // 如果不存在该队列的信息则新建 queueDataMap 存放到 topicQueueTable     List queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());     if (null == queueDataList) {         queueDataList = new LinkedList();         queueDataList.add(queueData);         this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList);         log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);     }     // 存在,直接更新替换旧的     else {         boolean addNewOne = true;          Iterator it = queueDataList.iterator();         while (it.hasNext()) {             QueueData qd = it.next();             if (qd.getBrokerName().equals(brokerName)) {                 if (qd.equals(queueData)) {                     addNewOne = false;                 } else {                     log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd,                         queueData);                     it.remove();                 }             }         }          if (addNewOne) {             queueDataList.add(queueData);         }     } }
  NameServer与Broker保持着长连接,Broker的状态信息存储在brokerLive-Table中,NameServer每收到一个心跳包,将更新brokerLiveTable中关于Broker的状态信息以及路由表(topicQueueTable、brokerAddrTable、brokerLiveTable、filterServer-Table)。更新上述路由表(HashTable)使用了锁粒度较少的读写锁,允许多个消息发送者并发读操作,保证消息发送时的高并发。同一时刻NameServer只处理一个Broker心跳包,多个心跳包请求串行执行。四、NameServer处理心跳流程分析
  主要是通过默认的请求处理组件DefaultRequestProcessor接收心跳请求,调用queryBrokerTopicConfig方法触发RouteInfoManager中的updateBrokerInfoUpdateTimestamp方法进行broker保活,更新时间戳;
  注:后续我们会分析默认请求处理组件DefaultRequestProcessor的源码;// 如果说你要是broker可以定期向你的nameserver进行心跳的话,每次心跳 // 都会更新一下broker保活数据的时间戳 public void updateBrokerInfoUpdateTimestamp(final String brokerAddr) {     BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr);     if (prev != null) {         prev.setLastUpdateTimestamp(System.currentTimeMillis());     } }五、路由删除流程分析
  NameServer会每隔10s扫描一次brokerLiveTable状态表,如果BrokerLive的lastUpdateTimestamp时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker的连接,同时更新topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable。
  RocketMQ有两个触发点来触发路由删除操作:NameServer定时扫描brokerLiveTable,检测上次心跳包与当前系统时间的时间戳,如果时间戳大于120s,则需要移除该Broker信息。Broker在正常关闭的情况下,会执行unregisterBroker指令移除该Broker信息。
  1、NameServer定时扫描brokerLiveTable
  每10s执行一次。逻辑也很简单,先遍历brokerLiveInfo路由表(HashMap),检测BrokerLiveInfo的LastUpdateTimestamp上次收到心跳包的时间,如果超过120s,则认为该Broker已不可用,然后将它移除并关闭连接,最后删除与该Broker相关的路由信息。// broker定时保活扫描,如果说你的broker机器跟nameserver之间超过2分钟没有通信 // 等于说关闭掉跟你的物理网络连接,以及清理掉内存数据结构里关于这个broker机器的数据 public void scanNotActiveBroker() {     // 扫描的就是这个BrokerLiveTable,路由信息表。还有一个Brokernames     Iterator> it = this.brokerLiveTable.entrySet().iterator();     while (it.hasNext()) {         Entry next = it.next();         long last = next.getValue().getLastUpdateTimestamp();         // 根据心跳时间判断是否存活的核心逻辑。两分钟未发送心跳注册请求         if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {             RemotingUtil.closeChannel(next.getValue().getChannel());             it.remove();             log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);             this.onChannelDestroy(next.getKey(), next.getValue().getChannel());         }     } }获取读锁,如果Channel不为空,就遍历brokerLiveTable尝试获取使用了该Channel的Broker。最后解锁。获取写锁,根据brokerAddr从brokerLiveTable、filterServerTable中移除Broker相关的信息。维护brokerAddrTable。遍历brokerAddrTable,从BrokerData的brokerAddrs中,找到具体的Broker,从BrokerData中将其移除。如果移除后在BrokerData中不再包含其他Broker,则在brokerAddrTable中移除该brokerName对应的条目。维护clusterAddrTable,也是遍历。找到Broker并将其从集群中基础。如果移除后,集群不包含任何Broker,则将该集群从clusterAddrTable中移除。维护topicQueueTable,遍历所有主题的队列,如果队列中包含要删除的Broker的队列,则移除,如果Topic只包含待移除Broker的队列,则从topicQueueTable删除该Topic释放写锁,完成路由删除操作。public void onChannelDestroy(String remoteAddr, Channel channel) {     String brokerAddrFound = null;     if (channel != null) {         try {             try {                 // 获取读锁                 this.lock.readLock().lockInterruptibly();                 Iterator> itBrokerLiveTable =                     this.brokerLiveTable.entrySet().iterator();                 // 遍历brokerLiveTable                 while (itBrokerLiveTable.hasNext()) {                     Entry entry = itBrokerLiveTable.next();                     // 获取使用该channel的brokerAddr                     if (entry.getValue().getChannel() == channel) {                         brokerAddrFound = entry.getKey();                         break;                     }                 }             } finally {                 this.lock.readLock().unlock();             }         } catch (Exception e) {             log.error("onChannelDestroy Exception", e);         }     }      // channel为空或者没有使用该channel的Broker     if (null == brokerAddrFound) {         brokerAddrFound = remoteAddr;     } else {         log.info("the broker"s channel destroyed, {}, clean it"s data structure at once", brokerAddrFound);     }      if (brokerAddrFound != null && brokerAddrFound.length() > 0) {          try {             try {                 // 申请写锁                 this.lock.writeLock().lockInterruptibly();                 // 根据brokerAddr从brokerLiveTable、filterServerTable中移除Broker相关的信息                 this.brokerLiveTable.remove(brokerAddrFound);                 this.filterServerTable.remove(brokerAddrFound);                 String brokerNameFound = null;                 boolean removeBrokerName = false;                 Iterator> itBrokerAddrTable =                     this.brokerAddrTable.entrySet().iterator();                 // 遍历 brokerAddrTable                 while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {                     BrokerData brokerData = itBrokerAddrTable.next().getValue();                      Iterator> it = brokerData.getBrokerAddrs().entrySet().iterator();                     while (it.hasNext()) {                         Entry entry = it.next();                         Long brokerId = entry.getKey();                         String brokerAddr = entry.getValue();                         // 移除该 brokerAddr的信息                         if (brokerAddr.equals(brokerAddrFound)) {                             brokerNameFound = brokerData.getBrokerName();                             it.remove();                             log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",                                 brokerId, brokerAddr);                             break;                         }                     }                      if (brokerData.getBrokerAddrs().isEmpty()) {                         removeBrokerName = true;                         itBrokerAddrTable.remove();                         log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",                             brokerData.getBrokerName());                     }                 }                  if (brokerNameFound != null && removeBrokerName) {                     Iterator>> it = this.clusterAddrTable.entrySet().iterator();                     // 遍历 clusterAddrTable                     while (it.hasNext()) {                         Entry> entry = it.next();                         String clusterName = entry.getKey();                         Set brokerNames = entry.getValue();                         boolean removed = brokerNames.remove(brokerNameFound);                         if (removed) {                             log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",                                 brokerNameFound, clusterName);                              // 成功移除Broker之后,集群不包含任何Broker,则将该集群从clusterAddrTable中移除                             if (brokerNames.isEmpty()) {                                 log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",                                     clusterName);                                 it.remove();                             }                              break;                         }                     }                 }                  if (removeBrokerName) {                     // 遍历 topicQueueTable                     Iterator>> itTopicQueueTable =                         this.topicQueueTable.entrySet().iterator();                     while (itTopicQueueTable.hasNext()) {                         Entry> entry = itTopicQueueTable.next();                         String topic = entry.getKey();                         List queueDataList = entry.getValue();                          Iterator itQueueData = queueDataList.iterator();                         while (itQueueData.hasNext()) {                             QueueData queueData = itQueueData.next();                             if (queueData.getBrokerName().equals(brokerNameFound)) {                                 itQueueData.remove();                                 log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",                                     topic, queueData);                             }                         }                          // 如果队列已经为空,移除该Topic                         if (queueDataList.isEmpty()) {                             itTopicQueueTable.remove();                             log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",                                 topic);                         }                     }                 }             } finally {                 // 释放写锁                 this.lock.writeLock().unlock();             }         } catch (Exception e) {             log.error("onChannelDestroy Exception", e);         }     } }
  2、执行unregisterBroker方法下线Broker// broker可以注册,也可以来进行下线 -> 集群+组+机器 public void unregisterBroker(     final String clusterName,     final String brokerAddr,     final String brokerName,     final long brokerId) {     try {         try {             this.lock.writeLock().lockInterruptibly();             BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddr);             log.info("unregisterBroker, remove from brokerLiveTable {}, {}",                 brokerLiveInfo != null ? "OK" : "Failed",                 brokerAddr             );              this.filterServerTable.remove(brokerAddr);              boolean removeBrokerName = false;             BrokerData brokerData = this.brokerAddrTable.get(brokerName);             if (null != brokerData) {                 String addr = brokerData.getBrokerAddrs().remove(brokerId);                 log.info("unregisterBroker, remove addr from brokerAddrTable {}, {}",                     addr != null ? "OK" : "Failed",                     brokerAddr                 );                  if (brokerData.getBrokerAddrs().isEmpty()) {                     this.brokerAddrTable.remove(brokerName);                     log.info("unregisterBroker, remove name from brokerAddrTable OK, {}",                         brokerName                     );                      removeBrokerName = true;                 }             }              if (removeBrokerName) {                 Set nameSet = this.clusterAddrTable.get(clusterName);                 if (nameSet != null) {                     boolean removed = nameSet.remove(brokerName);                     log.info("unregisterBroker, remove name from clusterAddrTable {}, {}",                         removed ? "OK" : "Failed",                         brokerName);                      if (nameSet.isEmpty()) {                         this.clusterAddrTable.remove(clusterName);                         log.info("unregisterBroker, remove cluster from clusterAddrTable {}",                             clusterName                         );                     }                 }                 this.removeTopicByBrokerName(brokerName);             }         } finally {             this.lock.writeLock().unlock();         }     } catch (Exception e) {         log.error("unregisterBroker Exception", e);     } }// 对一个broker把他管理的topic数据移除掉 private void removeTopicByBrokerName(final String brokerName) {     Iterator>> itMap = this.topicQueueTable.entrySet().iterator();     while (itMap.hasNext()) {         Entry> entry = itMap.next();          String topic = entry.getKey();         List queueDataList = entry.getValue();         Iterator it = queueDataList.iterator();         while (it.hasNext()) {             QueueData qd = it.next();             if (qd.getBrokerName().equals(brokerName)) {                 log.info("removeTopicByBrokerName, remove one broker"s topic {} {}", topic, qd);                 it.remove();             }         }          if (queueDataList.isEmpty()) {             log.info("removeTopicByBrokerName, remove the topic all queue {}", topic);             itMap.remove();         }     } }

三国志战略版官渡之战先锋服详解戎车,高级功能中垒虚最适合沮授我是百科阅览,内容不垂直不专注,跨领域跨类目,不迎合不孤独。所有内容都是原创,敬请阅读,你若抄袭,维权到底!三国志战略版官渡之战新剧本特别增加了战械,这个是一个新的战斗装备。为什么iPhone苹果手机的短板是什么?轻轻上滑,探索更多有趣内容。科技苹果手机并非不可战胜。手机通讯一直是苹果手机的最大短板,一直遭受到苹果用户的吐槽而荣耀背靠华为的技术支持,在各种通讯技术方面占有天然的优势。苹果存在手机拍摄还能这么玩?智云SMOOTH5上这几个功能体验太可了在手机相机刚刚面世的那段时间,拍出来的照片都是自带马赛克特效,在某些光线不足的场景下,更是看不清拍出来的内容,不过经过这二十来年的不断发展进步,手机的成像效果已经有了非常巨大的进步诺基亚新N88渲染图鸿蒙OS和2寸副屏,跟苹果比有几分胜算诺基亚手机在过去是非常出名的一个手机品牌,但是因为转型的时候,系统选择错了,使得该品牌被大家遗弃了。诺基亚要想再翻身,回到高端手机的领域,不是不行,就是非常艰难。近期有关于诺基亚新发布一款小米12,雷军将苹果华为三星OV全部吊打了个遍昨天,小米12正式发布,不得不说这次的小米12确实很有诚意,也学iPhone13一样加量降价,与小11相比,配置更高了,但价格更便宜了。当然,在发布会上,少不了吊打友商的环节,而这江湖人过年安排已拉满,除了官方一条龙,私人整活同样精彩一梦江湖(原楚留香手游)四周年庆典版本万物生的爆料,让无数江湖人对这场即将到来的狂欢期待万分。不同以往的过年安排,着实让小伙伴们眼前一亮,特别是首届国风游乐盛典金陵奇妙会,除了精彩基于安卓12,小米MIUI13开发版开始推送,首批用户已收到更新IT之家12月29日消息,昨日晚间,雷军正式发布了小米12系列新品以及MIUI13等,并宣布开发版内测第一批将在12月29日起陆续发布。现在MIUI开发版内测用户已经收到了第一批MvivoS12系列代言人王嘉尔化身迷局挑战官,为你解读超S专家报告近日,vivo官方发布出王嘉尔担任vivoX天猫大牌日联合的迷局挑战官。视频中的王嘉尔身穿一件深蓝色的紧身高领毛衣,一条黑色紧身裤以及黑色皮鞋。王嘉尔这次的穿搭十分简单,但还是难掩浙江首批省级夜间文化和旅游消费集聚区名单出炉这些地方入选小时新闻客户端近日,浙江省文化和旅游厅浙江省发展和改革委员会浙江省商务厅联合发布了关于公布第一批省级夜间文化和旅游消费集聚区名单的通知(以下简称通知),共有13家集聚区脱颖而出,成法式风发型慵懒又优雅?5个小技巧让你轻松拥有朋友们!妈妈们年轻时非常流行的鲨鱼夹,在这两年里又开始风靡了起来,不得不感叹时尚可真是个轮回。慵懒风把头发拧一拧,往后面随性一夹,旁边再垂下几小缕丝发,蓬松感满满,一秒get法式慵女生秋冬必收绒毛外套穿搭推荐,酷帅又吸睛,搭裤裙装都好用气温逐渐骤降,不少人开始翻出厚外套大衣来应付寒冷的天气。今天要来推荐既能保暖又能兼具时髦气息的绒毛外套,毛茸茸的材质绝对是相当适合御寒的穿搭,除了常见的款式,也推荐搭党们不同版型材
米饭馒头和面条,哪个更容易升血糖?主食吃对了,血糖才更稳定近些年的调查数据显示,中国作为糖尿病大国,高血糖患者在总人口中的占比已经达到了60,而且其中有11。6的糖尿病患者。血糖是什么?血糖值多少时代表高血糖?吃米饭馒头面条都会升血糖吗?五大联赛之西甲2022夏窗重要引援汇总(2022。08。05)西甲转会简述新赛季西甲三队换帅,分别是西班牙人巴伦西亚和毕尔巴鄂竞技。博尔达拉斯和马塞利诺两位西甲名帅下课赋闲,加图索和巴尔韦德走马上任。上游球队皇马仅有两笔引援,琼阿梅尼花费上亿立秋不补,秋冬吃苦,建议多吃养肺食物,滋阴润燥营养好马上就要立秋了,俗话说立秋不补,秋冬吃苦,立秋是秋天的开始,虽然现在天气还是很炎热,但也在向秋高气爽的天气慢慢靠近,经历过一个苦夏,很多朋友的体重都会下降,这个时候需要适当进补,否建议中老年人,若条件允许,多吃5样高营养菜,比吃保健品强如果你也喜欢美食,点击关注,每天不断更新精彩内容!导语建议中老年人,若条件允许,多吃5样高营养菜,比吃保健品强!相信大家也知道,每年到了当下这个季节的时候,天气越来越燥热了,温差还黑头痘痘闭口总找上你?皮肤有6种表现时,深层清洁就要做了很多人在夏天都会遇到这样的皮肤问题黑头粉刺似乎比秋冬来得更猛烈,不管卸妆洗脸做得多么到位,黑头闭口还是会频繁出现。夏天皮肤油脂分泌情况加剧,汗水与油脂混合的状态下,皮肤毛孔容易堵塞三伏天爱出汗,建议多吃这些高钾菜,补足精神平安健康过伏天三伏天爱出汗,建议多吃这些高钾菜,补足精神平安健康过伏天三伏天,天气闷热爱出汗,身体的很多钾离子也会随着汗液流失,在三伏天里我们需要多吃一些含钾量高的食材,以补充身体中流失的钾元素30岁后穿搭要优雅高级,多尝试这些日常搭配,轻松穿出女人味30岁后穿搭要优雅高级,多尝试这些贴近生活的日常穿搭,轻松穿出优雅与女人味各人住在各人的衣服里。生活的酸甜苦辣咸,自我的摇摆脆弱都在衣着里。这些搭配或许不能让人惊艳,不会让人看起来建议上了年纪的女人,少戴这3款帽子,容易拉低品位还被说装嫩不少女人随着年龄的增长,就会不知不觉把自己归入大妈,甚至老阿姨这一群体,以为自己的年龄大了,就不适合打扮得太时髦,太年轻,但实际上并非如此,反而是越随着年龄的增长,我们越需要去学习背靠背狂砍7021!科比传人却投詹皇门下没有他就没有现在的我北京时间8月5日,小科比库兹马已经加入了奇才队,并且在新的球队打出了非常精彩的表演。近期库兹马也是接受了一个专访,他参加了追梦格林的播客节目,并且公开提到了自己在湖人期间夺冠的经历奥运首金得主许海峰回家乡!65岁老到认不出,视力仅0。5住单位房我们常说岁月就是一把无情的杀猪刀,意思是说随着时间的流逝,任何人都会变得年老色衰,很是无情,而有谁还记得我国奥运会第一枚金牌得主许海峰?他用一枪不仅为祖国争光添彩,而且拉开了我国体疯了吧,一次全明星没进,35岁拿4600万年薪?给了就是垃圾合同啊今天,NBA薪资专家BobbyMarks,谈到了CJ麦科勒姆的薪资情况。当地时间周一,后者将有资格和鹈鹕队,签下一份3年1。39亿美元的顶薪。加上原合约还剩两年,一旦双方签约成功,