Upsert在Hudi中的实现分析

本文深入剖析Hudi的Upsert实现,从查找记录位置、更新逻辑到插入处理,揭示Hudi如何高效地处理数据更新和插入,确保无记录丢失。核心逻辑包括位置信息回推、插入更新操作以及对小文件的优化处理。
摘要由CSDN通过智能技术生成

介绍

Hudi支持Upsert语义,即将数据插入更新至Hudi数据集中,在借助索引机制完成数据查询后(查找记录位于哪个文件),再将该记录的位置信息回推至记录本身,然后对于已经存在于文件的记录使用UPDATE,而未存在于文件中的记录使用INSERT。本篇继续分析记录如何进行插入更新的。

分析

还是从HoodieBloomIndex#tagLocation开始进行分析,其核心代码如下。

public JavaRDD<HoodieRecord<T>> tagLocation(JavaRDD<HoodieRecord<T>> recordRDD, JavaSparkContext jsc,
      HoodieTable<T> hoodieTable) {
   
    ...
    // Lookup indexes for all the partition/recordkey pair
    JavaPairRDD<HoodieKey, HoodieRecordLocation> keyFilenamePairRDD =
        lookupIndex(partitionRecordKeyPairRDD, jsc, hoodieTable);
	...
    JavaRDD<HoodieRecord<T>> taggedRecordRDD = tagLocationBacktoRecords(keyFilenamePairRDD, recordRDD);
	...

    return taggedRecordRDD;
  }

经过lookupIndex方法后只是找出了哪些记录存在于哪些文件,此时在原始记录中还并未有位置信息,需要经过tagLocationBacktoRecords将位置信息回推到记录中,该方法核心代码如下

protected JavaRDD<HoodieRecord<T>> tagLocationBacktoRecords(
      JavaPairRDD<HoodieKey, HoodieRecordLocation> keyFilenamePairRDD, JavaRDD<HoodieRecord<T>> recordRDD) {
   
    JavaPairRDD<HoodieKey, HoodieRecord<T>> keyRecordPairRDD =
        recordRDD.mapToPair(record -> new Tuple2<>(record.getKey(), record));
    // Here as the recordRDD might have more data than rowKeyRDD (some rowKeys' fileId is null),
    // so we do left outer join.
    return keyRecordPairRDD.leftOuterJoin(keyFilenamePairRDD).values()
        .map(v1 -> getTaggedRecord(v1._1, Option.ofNullable(v1._2.orNull())));
  }

可以看到该方法的核心逻辑非常简单,先把最原始的记录进行一次变换(方便后续进行join操作),然后将变换的记录与之前已经查找的记录进行一次左外连接就完成了记录位置的回推操作(不得不感叹RDD太强大了)。

在完成位置信息回推后,就可以通过upsertRecordsInternal进行插入更新了,该方法核心代码如下

private JavaRDD<WriteStatus> upsertRecordsInternal(JavaRDD<HoodieRecord<T>> preppedRecords, String commitTime,
      HoodieTable<T> hoodieTable, final boolean isUpsert) {
   

    ...
    WorkloadProfile profile = null;
    if (hoodieTable.isWorkloadProfileNeeded()) {
   
      profile = new WorkloadProfile(preppedRecords);
      saveWorkloadProfileMetadataToInflight(profile, hoodieTable, commitTime);
    }

    // partition using the insert partitioner
    final Partitioner partitioner = getPartitioner(hoodieTable, isUpsert, profile);
    JavaRDD<HoodieRecord<T>> partitionedRecords = partition(preppedRecords, partitioner);
    JavaRDD<WriteStatus> writeStatusRDD = partitionedRecords.mapPartitionsWithIndex((partition, recordItr) -> {
   
      if (isUpsert) {
   
        return hoodieTable.handleUpsertPartition(commitTime, partition, recordItr, partitioner);
      } else {
   
        return hoodieTable.handleInsertPartition(commitTime, partition, recordItr, partitioner);
      }
    }, true).flatMap(List::iterator);

    return updateIndexAndCommitIfNeeded(writeStatusRDD, hoodieTable, commitTime);
  }

首先会对记录进行统计,如本次处理中每个分区插入、更新多少条记录,然后根据不同的表类型(Merge On Read/Copy On Write)来获取对应的Partitioner进行重新分区,这里以HoodieCopyOnWriteTable$UpsertPartitioner为例进行分析。构造该对象时会利用profile信息来进行必要的初始化。

UpsertPartitioner(WorkloadProfile profile) {
   
      ...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值