分布式 - 消息队列Kafka:Kafka生产者发送消息的分区策略

news2024/9/23 13:25:33

文章目录

      • 1. PartitionInfo 分区源码
      • 2. Partitioner 分区器接口源码
      • 3. 自定义分区策略
      • 4. 轮询策略 RoundRobinPartitioner
      • 5. 黏性分区策略 UniformStickyPartitioner
      • 6. hash分区策略
      • 7. 默认分区策略 DefaultPartitioner

分区的作用就是提供负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性。不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量。

除了提供负载均衡这种最核心的功能之外,利用分区也可以实现其他一些业务级别的需求,比如实现业务级别的消息顺序的问题。

生产者发送的消息实体 ProducerRecord 的构造方法:

在这里插入图片描述

我们发送消息时可以指定分区号,如果不指定那就需要分区器,这个很重要,一条消息该发往哪一个分区,关系到顺序消息问题。下面我们说说 Kafka 生产者的分区策略。所谓分区策略是决定生产者将消息发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持你自定义分区策略。

1. PartitionInfo 分区源码

/**
 * This is used to describe per-partition state in the MetadataResponse.
 */
public class PartitionInfo {
    // 表示该分区所属的主题名称。
    private final String topic;
    // 表示该分区的编号。
    private final int partition;
    // 表示该分区的领导者节点。
    private final Node leader;
    // 表示该分区的所有副本节点。
    private final Node[] replicas;
    // 表示该分区的所有同步副本节点。
    private final Node[] inSyncReplicas;
    // 表示该分区的所有离线副本节点。
    private final Node[] offlineReplicas;

    public PartitionInfo(String topic, int partition, Node leader, Node[] replicas, Node[] inSyncReplicas) {
        this(topic, partition, leader, replicas, inSyncReplicas, new Node[0]);
    }

    public PartitionInfo(String topic,
                         int partition,
                         Node leader,
                         Node[] replicas,
                         Node[] inSyncReplicas,
                         Node[] offlineReplicas) {
        this.topic = topic;
        this.partition = partition;
        this.leader = leader;
        this.replicas = replicas;
        this.inSyncReplicas = inSyncReplicas;
        this.offlineReplicas = offlineReplicas;
    }
    
    // ....
}

2. Partitioner 分区器接口源码

Kafka的Partitioner接口是用来决定消息被分配到哪个分区的。它定义了一个方法partition,该方法接收三个参数:topic、key和value,返回一个int类型的分区号,表示消息应该被分配到哪个分区。

public interface Partitioner extends Configurable {

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes The serialized key to partition on( or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes The serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);

    /**
     * This is called when partitioner is closed.
     */
    default void close() {}
}

Partitioner接口的实现类可以根据不同的业务需求来实现不同的分区策略,例如根据消息的键、值、时间戳等信息来决定分区。

这里的topic、key、keyBytes、value和valueBytes都属于消息数据,cluster则是集群信息。Kafka 给你这么多信息,就是希望让你能够充分地利用这些信息对消息进行分区,计算出它要被发送到哪个分区中。

3. 自定义分区策略

只要你自己的实现类定义好了 partition 方法,同时设置partitioner.class 参数为你自己实现类的 Full Qualified Name,那么生产者程序就会按照你的代码逻辑对消息进行分区。

① 实现自定义分区策略 DefinePartitioner:

public class MyPartitioner implements Partitioner {
    private final AtomicInteger counter = new AtomicInteger(0);

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 获取该 topic 可用的所有分区信息
        List<PartitionInfo> partitionInfos = cluster.availablePartitionsForTopic(topic);
        int size = partitionInfos.size();
        if(keyBytes==null){
            // 如果 keyBytes 为 null,表示该消息没有 key,此时采用 round-robin 的方式将消息均匀地分配到不同的分区中。
            // 每次调用 getAndIncrement() 方法获取计数器的当前值并自增,然后对可用分区数取模,得到该消息应该被分配到的分区编号。
            return counter.getAndIncrement() % size;
        }else{
            // 如果 keyBytes 不为 null,表示该消息有 key,此时采用 murmur2 哈希算法将 key 转换为一个整数值,并对可用分区数取模,得到该消息应该被分配到的分区编号。
            return Utils.toPositive(Utils.murmur2(keyBytes) % size);
        }
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

② 显式地配置生产者端的参数 partitioner.class:

public class CustomProducer01 {
    private static final String brokerList = "10.65.132.2:9093";
    private static final String topic = "test";

    public static Properties initConfig(){
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 使用自定义分区器
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());
        return properties;
    }

    public static void main(String[] args) {
        // kafka生产者属性配置
        Properties properties = initConfig();
        // kafka生产者发送消息,默认是异步发送方式
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, "你好,kafka,使用自定义分区器");
        kafkaProducer.send(producerRecord, new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if(e==null){
                    System.out.println("recordMetadata发送的分区为:"+recordMetadata.partition());
                }
            }
        });
        // 关闭资源
        kafkaProducer.close();
    }
}

4. 轮询策略 RoundRobinPartitioner

也称 Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始,即将其分配到分区 0,就像下面这张图展示的那样。

在这里插入图片描述

这就是所谓的轮询策略,轮询策略是 Kafka Java 生产者 API 默认提供的分区策略。如果你未指定partitioner.class参数,那么你的生产者程序会按照轮询的方式在主题的所有分区间均匀地“码放”消息。

轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。

轮询策略实现类为 RoundRobinPartitioner,实现源码:

/**
 * The "Round-Robin" partitioner
 * 
 * This partitioning strategy can be used when user wants 
 * to distribute the writes to all partitions equally. This
 * is the behaviour regardless of record key hash. 
 *
 */
public class RoundRobinPartitioner implements Partitioner {
    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

    public void configure(Map<String, ?> configs) {}

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 获取该 topic 所有的分区
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        int nextValue = nextValue(topic);
         // 获取该 topic 所有可用的分区
        List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
        if (!availablePartitions.isEmpty()) {
            // 取模,这样获取的就是一个轮询的方式,从可用的分区列表中获取分区
            // Utils.toPositive(nextValue) 的作用是将传入的参数 nextValue 转换为正数。
            // 如果 nextValue 是负数,则返回 0,否则返回 nextValue 的值。
            int part = Utils.toPositive(nextValue) % availablePartitions.size();
            return availablePartitions.get(part).partition();
        } else {
            // no partitions are available, give a non-available partition
            // 取模,这样获取的就是一个轮询的方式,从分区列表中获取分区
            return Utils.toPositive(nextValue) % numPartitions;
        }
    }

    // 在ConcurrentMap中插入一个键值对,如果该键不存在,则使用提供的函数计算值并将其插入到Map中。
    // 如果该键已经存在,则返回与该键关联的值。
    private int nextValue(String topic) {
        // 在ConcurrentMap中插入一个键值对,如果该键不存在,则使用AtomicInteger的默认值0初始化值
        // 如果该键已经存在,则返回与该键关联的AtomicInteger对象。
        AtomicInteger counter = topicCounterMap.computeIfAbsent(topic, k -> {
            return new AtomicInteger(0);
        });
        // 使用返回的AtomicInteger对象对值进行原子操作,增加值
        return counter.getAndIncrement();
    }

    public void close() {}

}

Kafka的RoundRobinPartitioner是一种分区策略,它将消息依次分配到可用的分区中。具体来说,它会维护一个计数器,每次将消息分配到下一个分区,直到计数器达到分区总数,然后重新从第一个分区开始分配。这种策略可以确保消息在所有分区中均匀分布,但可能会导致某些分区负载过重,因为它无法考虑分区的实际负载情况。

5. 黏性分区策略 UniformStickyPartitioner

黏性分区策略会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,Kafka再随机选一个分区进行使用(和上一次的分区不同)。 Sticky Partitioning Strategy 会随机地选择一个分区并会尽可能地坚持使用该分区——即所谓的粘住这个分区。

kafka 在发送消息的时候 , 采用批处理方案 , 当达到一批后进行分送 , 但是如果一批数据中有不同分区的数据 , 就无法放置到一个批处理中, 而老版本(2.4版本之前)的轮询策略方案 , 就会导致一批数据被分到多个小的批次中 , 从而影响效率 , 故在新版本中 , 采用这种粘性的划分策略。

UniformStickyPartitioner 实现源码:

/**
 * The partitioning strategy:
 * <ul>
 * <li>If a partition is specified in the record, use it
 * <li>Otherwise choose the sticky partition that changes when the batch is full.
 * 
 * NOTE: In constrast to the DefaultPartitioner, the record key is NOT used as part of the partitioning strategy in this 
 *       partitioner. Records with the same key are not guaranteed to be sent to the same partition.
 * 
 * See KIP-480 for details about sticky partitioning.
 */
public class UniformStickyPartitioner implements Partitioner {

    private final StickyPartitionCache stickyPartitionCache = new StickyPartitionCache();

    public void configure(Map<String, ?> configs) {}

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return stickyPartitionCache.partition(topic, cluster);
    }

    public void close() {}
    
    /**
     * If a batch completed for the current sticky partition, change the sticky partition. 
     * Alternately, if no sticky partition has been determined, set one.
     */
    public void onNewBatch(String topic, Cluster cluster, int prevPartition) {
        stickyPartitionCache.nextPartition(topic, cluster, prevPartition);
    }
}

分析 StickyPartitionCache 源码:

/**
 * An internal class that implements a cache used for sticky partitioning behavior. The cache tracks the current sticky
 * partition for any given topic. This class should not be used externally. 
 */
public class StickyPartitionCache {
    // ConcurrentMap类型的indexCache成员变量,用于存储主题和其对应的粘性分区。
    private final ConcurrentMap<String, Integer> indexCache;
    public StickyPartitionCache() {
        this.indexCache = new ConcurrentHashMap<>();
    }

    // 获取给定主题的当前粘性分区。如果该主题的粘性分区尚未设置,则返回下一个分区。
    public int partition(String topic, Cluster cluster) {
        Integer part = indexCache.get(topic);
        if (part == null) {
            return nextPartition(topic, cluster, -1);
        }
        return part;
    }

    // 获取给定主题的下一个粘性分区。 
    public int nextPartition(String topic, Cluster cluster, int prevPartition) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        // 获取给定主题的粘性分区
        Integer oldPart = indexCache.get(topic);
        Integer newPart = oldPart;
        // 如果该主题的粘性分区尚未设置,则计算粘性分区
        if (oldPart == null || oldPart == prevPartition) {
            // 1. 计算分区号
            
            // 如果没有可用分区,则从所有分区列表中随机选择一个可用分区
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() < 1) {
                Integer random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
                newPart = random % partitions.size();
            // 如果只有一个可用分区,则选择该分区
            } else if (availablePartitions.size() == 1) {
                newPart = availablePartitions.get(0).partition();
            // 从可用分区列表中随机选择一个分区
            } else {
                while (newPart == null || newPart.equals(oldPart)) {
                    int random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
                    newPart = availablePartitions.get(random % availablePartitions.size()).partition();
                }
            }
            
			// 2. 填充 indexCache
            
            if (oldPart == null) {
                indexCache.putIfAbsent(topic, newPart);
            } else {
                indexCache.replace(topic, prevPartition, newPart);
            }
            return indexCache.get(topic);
        }
        return indexCache.get(topic);
    }
}

6. hash分区策略

Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key 的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。特别是在 Kafka 不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进 Key 里面的。一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如下图所示。

在这里插入图片描述

7. 默认分区策略 DefaultPartitioner

Kafka 默认使用的分区器为 DefaultPartitioner,这是一个默认的分区策略实现类,其分区策略如下:

  • 如果记录中指定了分区,则使用该分区,不会调用分区器接口实现类。
  • 如果记录中没有指定分区但有key,则使用hash分区策略。
  • 如果记录中既没有指定分区也没有key,则 kafka 2.4版本前使用轮询策略,2.4版本后使用粘性分区策略。
/**
    The default partitioning strategy:
    If a partition is specified in the record, use it
    If no partition is specified but a key is present choose a partition based on a hash of the key
    If no partition or key is present choose the sticky partition that changes when the batch is full.
 */
public class DefaultPartitioner implements Partitioner {

    private final StickyPartitionCache stickyPartitionCache = new StickyPartitionCache();

    public void configure(Map<String, ?> configs) {}

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return partition(topic, key, keyBytes, value, valueBytes, cluster, cluster.partitionsForTopic(topic).size());
    }

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster,int numPartitions) {
        // 如果没有指定key,则使用粘性分区策略
        if (keyBytes == null) {
            return stickyPartitionCache.partition(topic, cluster);
        }
        // hash the keyBytes to choose a partition
        // Utils.murmur2(keyBytes) 是一个使用 MurmurHash2 算法计算给定字节数组的哈希值的方法。
        // 如果制定了key,则使用key的hash值对分区数取模得到分区。
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }

    public void close() {}
    
    /**
     * If a batch completed for the current sticky partition, change the sticky partition. 
     * Alternately, if no sticky partition has been determined, set one.
     */
    public void onNewBatch(String topic, Cluster cluster, int prevPartition) {
        stickyPartitionCache.nextPartition(topic, cluster, prevPartition);
    }
}

基于 kafka 3.0 版本:

① 如果记录中指定了分区,则使用该分区,此时不会进入任何分区器:

在这里插入图片描述

public class KafkaProducer<K, V> implements Producer<K, V> {
    // ...
    
	@Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        return doSend(interceptedRecord, callback);
    }

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
        try {
            // ...
            int partition = partition(record, serializedKey, serializedValue, cluster);
            // ...
        } 
    }
    
  /**
     * computes partition for given record.
     * if the record has partition returns the value otherwise calls configured partitioner class to compute the partition.
     */
    private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
        // 如果记录中指定了分区,则使用该分区,不会继续调用partitioner.partition()方法
        Integer partition = record.partition();
        return partition != null ?
                partition :
                partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
    }
}

② 如果记录中没有指定分区但有key,则会使用hash分区策略计算分区:

在这里插入图片描述

public class DefaultPartitioner implements Partitioner {

    private final StickyPartitionCache stickyPartitionCache = new StickyPartitionCache();

    public void configure(Map<String, ?> configs) {}

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return partition(topic, key, keyBytes, value, valueBytes, cluster, cluster.partitionsForTopic(topic).size());
    }

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster,int numPartitions) {
        if (keyBytes == null) {
            return stickyPartitionCache.partition(topic, cluster);
        }
        // hash the keyBytes to choose a partition
        // 使用key的hash值对分区数取模得到分区
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }

    public void close() {}

    public void onNewBatch(String topic, Cluster cluster, int prevPartition) {
        stickyPartitionCache.nextPartition(topic, cluster, prevPartition);
    }
}

在这里插入图片描述

③ 如果记录中既没有指定分区也没有key,则会使用粘性分区策略计算分区:

在这里插入图片描述
在这里插入图片描述

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

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

相关文章

【Shell编程】Linux Shell编程入门:输入输出、变量、条件判断、函数和运算

在Linux操作系统中&#xff0c;Shell编程是一项非常重要的技能。通过Shell编程&#xff0c;我们可以自动化执行各种任务&#xff0c;提高工作效率。无论是系统管理、数据处理还是软件开发&#xff0c;都离不开Shell脚本的应用。本篇博客将带领大家深入了解Shell编程的基础知识&…

木马免杀(篇一)基础知识学习

木马免杀&#xff08;篇一&#xff09;基础知识学习 ———— 简单的木马就是一个 exe 文件&#xff0c;比如今年hw流传的一张图&#xff1a;某可疑 exe 文件正在加载。当然木马还可能伪造成各式各样的文件&#xff0c;dll动态链接库文件、lnk快捷方式文件等&#xff0c;也可能…

音视频基础:分辨率、码率、帧率之间关系

基础 人类视觉系统 分辨率 像素&#xff1a; 是指由图像的小方格组成的&#xff0c;这些小方块都有一个明确的位置和被分配的色彩数值&#xff0c;小方格颜色和位置就决定该图像所呈现出来的样子&#xff1b;可以将像素视为整个图像中不可分割的单位或者是元素&#xff1b;像素…

php通过递归获取分公司的上下级数据

1.表结构 2.php核心代码 /*** param $branches 全部分公司数据* param $parentId 查询的分公司id&#xff0c;传0则全部排序。大于0&#xff0c;则查询该分公司下的下级* param int $level 层级&#xff0c;方便界面特效* param int $level_grade 层级叠加数* return array*/f…

CNN的特性

1、位移不变性 它指的是无论物体在图像中的什么位置&#xff0c;卷积神经网络的识别结果都应该是一样的。 因为CNN就是利用一个kernel在整张图像上不断步进来完成卷积操作的&#xff0c;而且在这个过程中kernel的参数是共享的。换句话说&#xff0c;它其实就是拿了同一张“通…

Docker+rancher部署SkyWalking8.5并应用在springboot服务中

1.Skywalking介绍 Skywalking是一个国产的开源框架&#xff0c;2015年有吴晟个人开源&#xff0c;2017年加入Apache孵化器&#xff0c;国人开源的产品&#xff0c;主要开发人员来自于华为&#xff0c;2019年4月17日Apache董事会批准SkyWalking成为顶级项目&#xff0c;支持Jav…

预测成真,国内传来三个消息,中国年轻人变了,创新力产品崛起

中国的年轻人真的变了&#xff01; 最近&#xff0c;国内传来三个消息&#xff0c;让外媒的预测成真。 第一&#xff0c;奥迪要开始用国产车的平台了。这里需要说明的是新能源汽车&#xff0c;奥迪也曾多次公开表示&#xff0c;承认了当前中国新能源汽车核心技术上的领先。 第…

【计算机网络】概述及数据链路层

每一层只依赖于下一层所提供的服务&#xff0c;使得各层之间相互独立、灵活性好&#xff0c;已于实现和维护&#xff0c;并能促进标准化工作。 应用层&#xff1a;通过应用进程间的交互完成特定的网络应用&#xff0c;HTTP、FTP、DNS&#xff0c;应用层交互的数据单元被称为报…

java编程规范

一、时间格式为什么有大写有小写呢&#xff1f; new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");为了区分月份和分钟&#xff0c;用大写M代表月份&#xff0c;小写m代表分钟 而大写的H代表24小时制&#xff0c;小写h代表12小时制 二、下面的程序判断等值的方式&…

【人工智能前沿弄潮】—— SAM自动生成物体mask

SAM自动生成物体mask 由于SAM可以高效处理提示&#xff0c;可以通过在图像上抽样大量的提示来生成整个图像的mask。这种方法被用来生成数据集SA-1B。 类SamAutomaticMaskGenerator实现了这个功能。它通过在图像上的网格中对单点输入提示进行抽样&#xff0c;从每个提示中SAM可…

基于关系有向图的知识推理2022ACM 8.9

基于关系有向图的知识推理 摘要介绍相关工作基于路径的方法基于GNN的方法 关系有向图RED-GCN实验 摘要 知识图推理旨在从已有的知识中推断出新的事实。基于关系路径的方法在文献中显示出较强的可解释性和归纳推理能力。然而&#xff0c;在KG中 捕获复杂拓扑(Capturing complex…

饮用水除硝酸盐、饮用水除砷、饮用水除氟、饮用水除铁锰的技术汇总

我们所说的“自来水”是指从水龙头里放出来的水。但从水龙头里放出来并不等于安全卫生。实际上&#xff0c;原水必须经过各种处理措施之后才能称为安全卫生的饮用水。每一滴水都要经过了混凝、沉淀、过滤、消毒四个步骤的处理&#xff0c;才能去除杂质和细菌&#xff0c;变得安…

Three.js纹理贴图

目录 Three.js入门 Three.js光源 Three.js阴影 Three.js纹理贴图 纹理是一种图像或图像数据&#xff0c;用于为物体的材质提供颜色、纹理、法线、位移等信息&#xff0c;从而实现更加逼真的渲染结果。 纹理可以应用于Three.js中的材质类型&#xff0c;如MeshBasicMaterial…

本质矩阵E、基本矩阵F、单应矩阵H

1. E (归一化坐标对进行计算) t ^ R 为3*3的矩阵, 因为R,t共有6个自由度&#xff0c;又因为单目尺度等价性&#xff0c;所以实际上E矩阵共有5个自由度。因此至少需要5个点对来求解。 2. 基本矩阵F:根据两帧间匹配的像素点对儿计算 3*3且自由度为7的矩阵kF也为基础矩阵&#x…

构建之法 - 软工教学:每天都向前推进一点点

作者&#xff1a;福州⼤学 汪璟玢⽼师 汪老师&#xff1a;每次都向前推进一点点&#xff0c;哪怕只有一点点&#xff0c;也好过什么都不做。 ​邹老师&#xff1a;对&#xff0c;几个学期下来&#xff0c;就已经超过那些“空想”的团队很远了。坚持下去&#xff01; 汪老师&…

x86 kgdb deug调试分析

本文主要是收集&#xff0c;以下文章写得很好&#xff0c;我二次整理一下。 如果要手动livedb. 1. call kdbg_arch_late() 2. kgd_set_hw_break(addr,8,1); 3. kgdb_correct_hw_break();// enable bp to cpu regs -------------------------------分割线----------------…

第5讲:如何构建类的方法

【分享成果&#xff0c;随喜正能量】在这个社会上&#xff0c;对别人好一点&#xff0c;多站在别人的角度考虑&#xff0c;不要为小事争执&#xff0c;不要取笑他人&#xff0c;不要在别人背后嚼舌根&#xff0c;更不能逼人太甚。凡事退一步&#xff0c;对你有好处。。 《VBA中…

前沿分享-无创检测血糖RF波

非侵入性血糖仪&#xff0c;利用射频 (RF) 波连续测量血液中的葡萄糖水平。利用射频波技术连续实时监测血液中的葡萄糖水平&#xff0c;使用的辐射要比手机少得多。 大概原理是血液中的葡萄糖是具有介电特性&#xff0c;一般来说就是介电常数。 电磁波波幅的衰减反映了介质对电…

电脑文件丢失如何找回?使用这个方法轻松找回!

电脑文件丢失怎么办&#xff1f;有没有免费的电脑文件恢复软件&#xff1f;相信很多人在日常办公中也都经常会遇到这种现象&#xff0c;不管是在学习中&#xff0c;还是日常的办公&#xff0c;往往也都会在电脑上存储大量的数据文件&#xff0c;那么如果我们在日常办公操作过程…

忆恒创源发布PBlaze7 7940系列PCIe 5.0企业级NVMe SSD

今天&#xff0c;国内知名企业级SSD产品和解决方案供应商——北京忆恒创源科技股份有限公司&#xff08;Memblaze&#xff0c;以下简称“忆恒创源”&#xff09;全新一代PCIe 5.0企业级NVMe SSD PBlaze7 7940正式发布。与主流PCIe 4.0产品相比&#xff0c;PBlaze7 7940有着2.5倍…