Paimon源码解读 -- PartialUpdateMerge

一.父接口MergeFunction

其实现子类如下图

可以看到,Paimon中所有的Merge Engine都实现了MergeFunction接口,那么继续看该接口中的4个抽象方法

java 复制代码
public interface MergeFunction<T> {

    /**
     * 重置方法,该方法在add()前或getResult()后被调用
     */
    void reset();

    /**
     * 合并方法:将输入的kv加入到合并function中,进行合并
     * @param kv
     */
    void add(KeyValue kv);

    /**
     * 获取结果:获取当前合并后的结果
     * @return
     */
    T getResult();

    /**
     * 缓存:按需复制输入来的kv,放入内存中
     * @return
     */
    boolean requireCopy();
}

二.PartialUpdateMergeFunction

1.属性和关键函数

(1) 属性和构造函数

java 复制代码
    public static final String SEQUENCE_GROUP = "sequence-group"; // 序列组的标识字符串,后续会判断有没有配置sequence-group
    // 1.配置相关
    private final InternalRow.FieldGetter[] getters; // 字段取值器,可以理解为反射获取Row中字段的值
    private final boolean ignoreDelete; // 是否忽略DEKETE类型,这也是和配置'ignore-delete'绑定
    private final List<WrapperWithFieldIndex<FieldsComparator>> fieldSeqComparators; // 比较器,用于决定聚合操作中的文件的聚合顺序
    private final boolean fieldSequenceEnabled; // 是否启用了field-sequnce,和配置'sequnce-group'绑定,默认是none
    private final List<WrapperWithFieldIndex<FieldAggregator>> fieldAggregators; // field聚合器,和配置'fields.xxx.aggregate-function'绑定
    private final boolean removeRecordOnDelete; // 当rowkind收到-D,是否移除该行数据,和配置'partial-update.remove-record-on-delete'绑定,默认是false不移除
    private final Set<Integer> sequenceGroupPartialDelete; // 当sequnce-group-delete绑定的字段收到-D,是否移除改行字段,和配置'partial-update.remove-record-on-sequence-group'绑定,默认是none,不绑定sg的delete字段
    private final boolean[] nullables; // 标识每个字段是否允许为null
    // 2.状态相关
    private InternalRow currentKey; // 当前正在处理的Key主键
    private long latestSequenceNumber; // 当前该Key下记录的最新sequnceNumber序列号
    private GenericRow row; // 用于缓存聚合中间结果的对象
    private KeyValue reused; // 复用的KV对象
    private boolean currentDeleteRow; // 标记当前聚合的结果是否为Delete类型
    private boolean notNullColumnFilled; // 标记是否已经填充了非null列

    /**
     * 如果首个值为回退retract操作,且未收到insert记录,则该行的rowkind应该为RowKind.DELETE
     * 注意:如果没有收到RowKind.INSERT的数据,那么pu表的seuqnce-group可能无法正确设置currentDeleteRow
     */
    private boolean meetInsert; // 标记是否遇到过 INSERT 类型的记录,用于处理复杂的删除逻辑。

    protected PartialUpdateMergeFunction(
            InternalRow.FieldGetter[] getters,
            boolean ignoreDelete,
            Map<Integer, FieldsComparator> fieldSeqComparators,
            Map<Integer, FieldAggregator> fieldAggregators,
            boolean fieldSequenceEnabled,
            boolean removeRecordOnDelete,
            Set<Integer> sequenceGroupPartialDelete,
            boolean[] nullables) {
        this.getters = getters;
        this.ignoreDelete = ignoreDelete;
        this.fieldSeqComparators = getKeySortedListFromMap(fieldSeqComparators);
        this.fieldAggregators = getKeySortedListFromMap(fieldAggregators);
        this.fieldSequenceEnabled = fieldSequenceEnabled;
        this.removeRecordOnDelete = removeRecordOnDelete;
        this.sequenceGroupPartialDelete = sequenceGroupPartialDelete;
        this.nullables = nullables;
    }

(2) fieldSeqComparators的创建方法

该fieldSeqComparators是在Factory的构造函数中创建的,很长,我们就看相关代码即可

这是关键点!!!

java 复制代码
/* 案例如下
"fields.se_sign.sequence-group" : "uids,pids,views",
"fields.views.aggregate-function" : "sum",
*/
for (Map.Entry<String, String> entry : options.toMap().entrySet()) {
    String k = entry.getKey(); // "fields.se_sign.sequence-group"
    String v = entry.getValue(); // "uids,pids,views"
    if (k.startsWith(FIELDS_PREFIX) && k.endsWith(SEQUENCE_GROUP)) {
        List<String> sequenceFields =
                Arrays.stream(
                                k.substring(
                                                FIELDS_PREFIX.length() + 1,
                                                k.length()
                                                        - SEQUENCE_GROUP.length()
                                                        - 1)
                                        .split(FIELDS_SEPARATOR))
                        .map(fieldName -> validateFieldName(fieldName, fieldNames))
                        .collect(Collectors.toList()); // [se_sign]
        allSequenceFields.addAll(sequenceFields); // [se_sign]

        Supplier<FieldsComparator> userDefinedSeqComparator =
                () -> UserDefinedSeqComparator.create(rowType, sequenceFields, true); // se_sign的比较器
        Arrays.stream(v.split(FIELDS_SEPARATOR))
                .map(
                        fieldName ->
                                fieldNames.indexOf(
                                        validateFieldName(fieldName, fieldNames)))
                .forEach(// 遍历[uids,pids,views]的index
                        field -> {
                            if (fieldSeqComparators.containsKey(field)) {
                                throw new IllegalArgumentException(
                                        String.format(
                                                "Field %s is defined repeatedly by multiple groups: %s",
                                                fieldNames.get(field), k));
                            }
                            fieldSeqComparators.put(field, userDefinedSeqComparator);
                            /* 生成一个map三个键值对
                              uids的index -> se_sign的比较器
                              pids的index -> se_sign的比较器
                              views的index -> se_sign的比较器
                             */
                        });

        // add self
        sequenceFields.forEach(
                fieldName -> {
                    int index = fieldNames.indexOf(fieldName);
                    fieldSeqComparators.put(index, userDefinedSeqComparator); // 最后放se_sign的index -> se_sign的比较器
                    sequenceGroupMap.put(fieldName, index);
                });
    }
}

2.reset()重置方法

java 复制代码
    /**
     * 重置合并函数的所有内部状态
     */
    @Override
    public void reset() {
        this.currentKey = null;
        this.meetInsert = false;
        this.notNullColumnFilled = false;
        this.row = new GenericRow(getters.length); // 创建一个新的GenericRow对象作为合并结果的载体,长度与字段数量(getters.length)一致。
        // 注意:这里row是创建新对象而非复用旧对象,避免旧分组的数据残留影响新分组的结果
        this.latestSequenceNumber = 0;
        fieldAggregators.forEach(w -> w.getValue().reset()); // 遍历所有字段聚合器(FieldAggregator),并调用它们各自的reset()方法。这是因为聚合器(如 SUM、MAX 等)自身也维护着中间状态(例如累加和),需要为新分组重置这些状态。
    }

3.add()合并方法 -- 核心逻辑

(1) add()

  • kv:当前来的数据
  • row:缓存的中间聚合后的数据
java 复制代码
    /**
     * 合并逻辑的核心操作
     * @param kv
     */
    @Override
    public void add(KeyValue kv) {
        // refresh key object to avoid reference overwritten
        currentKey = kv.key();
        currentDeleteRow = false;
        // 1.处理回撤记录,就是处理RowKind为-U或-D的
        if (kv.valueKind().isRetract()) {
            // 初始化空行:如果还未填充非空列,则调initRow()初始化进行赋值
            if (!notNullColumnFilled) {
                initRow(row, kv.value());
                notNullColumnFilled = true;
            }

            // In 0.7- versions, the delete records might be written into data file even when
            // ignore-delete configured, so ignoreDelete still needs to be checked
            // 解决Paimon0.7版本的bug,当时配置了ignore-delete不会生效,因此,这里仍然要判断一下,实现配置了ignore-delete=true 不处理
            // CASE-1: 若配置了ignore-delete为true,则直接跳过该条数据的处理
            if (ignoreDelete) {
                return;
            }

            latestSequenceNumber = kv.sequenceNumber(); // 更新latestSequenceNumber

            // CASE-2: 若开启了field-sequnece,也就是配置了'sequence-group',则调retractWithSequenceGroup(),进行更细化的回撤处理
            if (fieldSequenceEnabled) {
                retractWithSequenceGroup(kv);
                return;
            }
            // CASE-3: 若配置了partial-update.remove-record-on-delete为true,且当前RowKink是-D,那么调initRow(),并标记currentDeleteRow = true 重置该行数据
            if (removeRecordOnDelete) {
                if (kv.valueKind() == RowKind.DELETE) {
                    currentDeleteRow = true;
                    row = new GenericRow(getters.length);
                    initRow(row, kv.value());
                }
                return;
            }
            // 最后,若什么都没配置,则抛出异常,提醒用户必须选择一种回撤流的处理方式
            String msg =
                    String.join(
                            "\n",
                            "By default, Partial update can not accept delete records,"
                                    + " you can choose one of the following solutions:",
                            "1. Configure 'ignore-delete' to ignore delete records.",
                            "2. Configure 'partial-update.remove-record-on-delete' to remove the whole row when receiving delete records.",
                            "3. Configure 'sequence-group's to retract partial columns. Also configure 'partial-update.remove-record-on-sequence-group' to remove the whole row when receiving deleted records of `specified sequence group`.");

            throw new IllegalArgumentException(msg);
        }
        // 处理正常流,也就是RowKind为+I、+U的数据
        latestSequenceNumber = kv.sequenceNumber(); // 更新latestSequenceNumber
        if (fieldSeqComparators.isEmpty()) { // 若没有字段级的比较器,则调updateNonNullFields()去非空覆盖
            updateNonNullFields(kv);
        } else { // 若配置了字段级的比较器,则调updateNonNullFields()去按sequnce-group去更新
            updateWithSequenceGroup(kv); // 这是重点
        }
        meetInsert = true;
        notNullColumnFilled = true;
    }

(2) 调用的initRow()updateNonNullFields()

这俩方法都比较简单,就是初始化赋值,字段不允许为null,但是field值为null,则抛出异常 其实就是非空覆盖

疑问:为什么这里是覆盖,而不能聚合呢?那如果我就是建pu表的时候配置了聚合函数呢? 解答 :1.3版本后,若partial-update表里面要配置aggregate-function,必须用sequnce-group指定,否则DDL就会报错[ERROR] Could not execute SQL statement. Reason: java.lang.IllegalArgumentException: Must use sequence group for aggregation functions but not found for field views.

java 复制代码
    // 初始化赋值,字段不允许为null,但是field值为null,则抛出异常
    private void initRow(GenericRow row, InternalRow value) {
        // 遍历Row类型的每一个字段,检查是否允许为null,不允许,则抛异常
        for (int i = 0; i < getters.length; i++) {
            Object field = getters[i].getFieldOrNull(value); // 该行数据对应位置的字段值,若没有,则给null。
            if (!nullables[i]) {
                if (field != null) {
                    row.setField(i, field); // 给该字段进行赋值
                } else {
                    throw new IllegalArgumentException("Field " + i + " can not be null");
                }
            }
        }
    }
    
    // 逻辑和initRow类似,都是字段不允许null,但field值为null,则抛出异常
    private void updateNonNullFields(KeyValue kv) {
        for (int i = 0; i < getters.length; i++) {
            Object field = getters[i].getFieldOrNull(kv.value());
            if (field != null) {
                row.setField(i, field);
            } else {
                if (!nullables[i]) {
                    throw new IllegalArgumentException("Field " + i + " can not be null");
                }
            }
        }
    }

(3) 调用的updateWithSequenceGroup() -- 核心1

该方法是针对配置了sequnce-group的一个合并逻辑,并且也涉及到pu模型的aggregate-function聚合操作 -- 重点!!!

案例:

"fields.se_sign.sequence-group" : "uids,pids,views",

"fields.views.aggregate-function" : "sum",

那么,sequnce-group的标记字段为:se_sign,绑定字段为:uids、pids、views

流程如下:重点!!!

  1. 迭代sequnce-group的标记字段的比较器,这里是se_sign;
  2. 迭代field-aggregate-function字段的聚合器,这里是views的sum;
  3. 遍历所有字段进行比较和聚合处理:
    1. 匹配找到当前位置i的字段对应的比较器和聚合器;

    2. 获取i位置字段当前的累加和,row中的i位置字段值;

    3. CASE-1: 处理没有匹配到的比较器的字段,说明该字段没有被sequnce-group绑定,因此直接进行更新(有聚合,就聚合;没有就直接覆盖);

    4. CASE-2:处理有匹配到的比较器的字段:

      • 若当前行数据的sequnce-group标记字段为null,则直接跳过绑定字段和标记字段的全部处理;
      • 若当前记录kv的序列值 >= 缓存行row的序列值,执行聚合更新,当前行的se_sign的值 >= row中的se_sign值;
        1. 当前字段是sequnce-group的标记字段,如se_sign,则进行原子性操作;
        2. 当前字段是sequnce-group的绑定字段,如uids、views,则直接执行覆盖或聚合操作;
      • 若当前记录kv的序列 < 缓存行row的序列,但有聚合器,则仍然进行聚合;

      这里个人认为是一个BUG,后续希望社区能修改吧,其实可以开启一个开关,针对2种不同情况

      1. 有的业务是不允许覆盖维度,但是指标还是要的;
      2. 有的业务是维度和指标原子绑定,要么一起更新,要么全都不更新;
java 复制代码
    private void updateWithSequenceGroup(KeyValue kv) {
        /* 例如:
        "fields.se_sign.sequence-group" : "uids,pids,views",
        "fields.views.aggregate-function" : "sum",
         */
        // 1.迭代sequnce-group的标记字段的比较器,就是se_sign
        Iterator<WrapperWithFieldIndex<FieldsComparator>> comparatorIter =
                fieldSeqComparators.iterator(); // se_sign
        WrapperWithFieldIndex<FieldsComparator> curComparator =
                comparatorIter.hasNext() ? comparatorIter.next() : null;
        // 2.迭代field-aggregate-function字段的聚合器
        Iterator<WrapperWithFieldIndex<FieldAggregator>> aggIter = fieldAggregators.iterator(); // views
        WrapperWithFieldIndex<FieldAggregator> curAgg = aggIter.hasNext() ? aggIter.next() : null;
        // 记录哪些字段所属的sequnce-group为null,避免对同一个空sg组进行重复处理
        boolean[] isEmptySequenceGroup = new boolean[getters.length];
        // 3.遍历所有字段进行比较和聚合处理
        for (int i = 0; i < getters.length; i++) {
            // 3-1:为当前字段匹配对应的比较器和聚合器
            FieldsComparator seqComparator = null;
            if (curComparator != null && curComparator.fieldIndex == i) {
                seqComparator = curComparator.getValue();
                curComparator = comparatorIter.hasNext() ? comparatorIter.next() : null;
            }

            FieldAggregator aggregator = null;
            if (curAgg != null && curAgg.fieldIndex == i) {
                aggregator = curAgg.getValue();
                curAgg = aggIter.hasNext() ? aggIter.next() : null;
            }

            Object accumulator = row.getField(i); // 获取i位置字段当前的累加和
            /* 这里的row是当前主键分组下正在构建的合并结果行
            当处理属于同一个主键的多条记录时,所有更新操作都会累积到这个 row 对象上
            当切换到下一个主键分组时,reset() 会创建一个新的 row,旧的 row 会被丢弃或输出
             */
            // 3-2:处理没有匹配的比较器的字段
            if (seqComparator == null) {
                Object field = getters[i].getFieldOrNull(kv.value());
                if (aggregator != null) { // 若有聚合器,则执行相应的聚合操作,如SUM、MAX等
                    row.setField(i, aggregator.agg(accumulator, field));
                } else if (field != null) { // 无聚合器,但字段不为null,则覆盖 其实就相当于默认是非空覆盖
                    row.setField(i, field);
                }
            } else { // 3-3:处理有匹配的比较器的字段
                // CASE-1: 如果sequnce-group的标记字段是null,则跳过该绑定字段的处理,比如当前行中uids、pids、views的se_sign为null,则会跳过这三个字段的处理
                if (isEmptySequenceGroup(kv, seqComparator, isEmptySequenceGroup)) {
                    // skip null sequence group
                    continue; // 跳过的是当前sequence-group标记字段的处理
                }
                // 当sequnce-group的标记字段不为null的情况进行处理,se_sign不为null
                Object field = getters[i].getFieldOrNull(kv.value());
                // CASE-2: 当前记录的序列 >= 缓存行的序列,执行聚合更新,就是比较se_sign的值
                if (seqComparator.compare(kv.value(), row) >= 0) {
                    int index = i;

                    // Multiple sequence fields should be updated at once.
                    // 针对多个sequnce-group标记字段,如"fields.a,b.sequence-group" : "d,e,f", 那么就会对a和b进行原子操作
                    // 如果当前i位置的字段是sequnce-group的标记字段,如se_sign,那么就会执行下面的if+for的原子性操作
                    if (Arrays.stream(seqComparator.compareFields())
                            .anyMatch(seqIndex -> seqIndex == index)) {
                        for (int fieldIndex : seqComparator.compareFields()) {
                            row.setField(
                                    fieldIndex, getters[fieldIndex].getFieldOrNull(kv.value()));
                        }
                        continue; // 跳过的是当前sequence-group标记字段的处理
                    }
                    /* 更新聚合操作
                        1.有聚合器,按照聚合逻辑去操作;
                        2.无聚合器,直接覆盖
                        注意:这里的覆盖不是非空覆盖,因此,为了解决null的覆盖问题,需要配置"fields.default-aggregate-function" : "last_non_null_value", 因为默认是none
                     */
                    row.setField(
                            i, aggregator == null ? field : aggregator.agg(accumulator, field));
                }
                // CASE-3: 当前记录的序列 < 结果行的序列,但有聚合器,仍然进行聚合,这个aggReversed(accumulator, field)其实就是agg(field,accumulator)参数位置换了,没区别
                else if (aggregator != null) {
                    row.setField(i, aggregator.aggReversed(accumulator, field));
                }
            }
        }
    }

(4) 调用的isEmptySequenceGroup()

该方法主要用于判断sequnce-group的标记字段是否全为null的

注意:只有全部的标记字段都为null才会进入缓存,然后再处理后续的标记字段时,只需要用缓存判断第一个字段是否为true即可

java 复制代码
    private boolean isEmptySequenceGroup(
            KeyValue kv, FieldsComparator comparator, boolean[] isEmptySequenceGroup) {

        // If any flag of the sequence fields is set, it means the sequence group is empty.
        // CASE-1. 缓存命中:快速判断序列组第一个标记字段是否为空,为空,则直接return true了
        // 这里需要结合CASE-3才能解释,只有全部的标记字段都为null才会进入缓存,然后再处理后续的标记字段时,只需要用缓存判断第一个字段是否为true即可
        if (isEmptySequenceGroup[comparator.compareFields()[0]]) {
            return true;
        }
        // CASE-2.遍历全部的sequnce-group的标记字段,进行能判空,有一个不为null,则return false;
        for (int fieldIndex : comparator.compareFields()) {
            if (getters[fieldIndex].getFieldOrNull(kv.value()) != null) {
                return false;
            }
        }

        // Set the flag of all the sequence fields of the sequence group.
        // CASE-3.缓存全部为null的sequnce-group标记字段
        for (int fieldIndex : comparator.compareFields()) {
            isEmptySequenceGroup[fieldIndex] = true;
        }

        return true;
    }

(5) 调用的retractWithSequenceGroup() -- 核心2

代码处理逻辑和updateWithSequenceGroup()类似,只是最后处理有点不同,涉及到回撤操作

还是以上面的案例

  1. 迭代sequnce-group的标记字段的比较器,这里是se_sign;
  2. 迭代field-aggregate-function字段的聚合器,这里是views的sum;
  3. 遍历所有字段进行比较和聚合处理:
    1. 匹配找到当前位置i的字段对应的比较器和聚合器;
    2. 只处理有匹配的比较器的字段,因为只有配置了sequnce-group的才会走到这个方法内
      • 若当前行数据的sequnce-group标记字段为null,则直接跳过绑定字段和标记字段的全部处理;

      • 若当前记录kv的序列值 >= 缓存行row的序列值,执行聚合更新,当前行的se_sign的值 >= row中的se_sign值;

        1. 回撤标记字段 ,如se_sign,则调init()实现原子性回撤操作;
        2. 回撤其他字段 ;无聚合器如uids,用null覆盖;有聚合器如views,调aggregator.retract()去进行回撤

        底层如sum是做减法,max和min是没有回撤的,调FieldAggregator的会抛出异常,具体看FieldAggregator的子类

      • 若当前记录kv的序列 < 缓存行row的序列,但有聚合器,调aggregator.retract()去进行回撤;

注意: 即使我们配置了"fields.default-aggregate-function" : "last_non_null_value"回撤的时候仍然是null,因为FieldLastNonNullValueAgg函数的retract就是回撤流的字段值非空则给null,空给accumulator

java 复制代码
    private void retractWithSequenceGroup(KeyValue kv) {
        // 0.初始化updatedSequenceFields
        Set<Integer> updatedSequenceFields = new HashSet<>();
        // 1.迭代比较器,如se_sign
        Iterator<WrapperWithFieldIndex<FieldsComparator>> comparatorIter =
                fieldSeqComparators.iterator();
        WrapperWithFieldIndex<FieldsComparator> curComparator =
                comparatorIter.hasNext() ? comparatorIter.next() : null;
        // 2.迭代聚合器,如views的sum
        Iterator<WrapperWithFieldIndex<FieldAggregator>> aggIter = fieldAggregators.iterator();
        WrapperWithFieldIndex<FieldAggregator> curAgg = aggIter.hasNext() ? aggIter.next() : null;
        // 记录哪些字段所属的sequnce-group为null,避免对同一个空sg组进行重复处理,每一轮都会new一个新的,防止缓存影响
        boolean[] isEmptySequenceGroup = new boolean[getters.length];
        // 3.遍历所有字段进行比较和聚合处理
        for (int i = 0; i < getters.length; i++) {
            // 3-1:为当前字段匹配对应的比较器和聚合器
            FieldsComparator seqComparator = null;
            if (curComparator != null && curComparator.fieldIndex == i) {
                seqComparator = curComparator.getValue();
                curComparator = comparatorIter.hasNext() ? comparatorIter.next() : null;
            }

            FieldAggregator aggregator = null;
            if (curAgg != null && curAgg.fieldIndex == i) {
                aggregator = curAgg.getValue();
                curAgg = aggIter.hasNext() ? aggIter.next() : null;
            }
            // 3-2:只处理有匹配的比较器的字段,因为只有配置了sequnce-group的才会走到这个方法内
            if (seqComparator != null) {
                // CASE-1: 如果sequnce-group的标记字段是null,则跳过该绑定字段的处理,比如当前行中uids、pids、views的se_sign为null,则会跳过这三个字段的处理
                if (isEmptySequenceGroup(kv, seqComparator, isEmptySequenceGroup)) {
                    // skip null sequence group
                    continue;
                }
                // CASE-2: 当前记录kv的序列 >= 缓存行row的序列,执行聚合更新,就是比较se_sign的值
                if (seqComparator.compare(kv.value(), row) >= 0) {
                    int index = i;

                    // Multiple sequence fields should be updated at once.
                    // (1) 原子性回撤更新序列组内的标记字段
                    if (Arrays.stream(seqComparator.compareFields())
                            .anyMatch(field -> field == index)) {
                        for (int field : seqComparator.compareFields()) {
                            // 没处理过该标记字段,则进行处理
                            if (!updatedSequenceFields.contains(field)) {
                                // 若当前的RowKind为-D,且配置的partial-update.remove-record-on-sequence-group字段为当前的标记field,则进行初始化row,然后return出去
                                if (kv.valueKind() == RowKind.DELETE
                                        && sequenceGroupPartialDelete.contains(field)) {
                                    currentDeleteRow = true;
                                    row = new GenericRow(getters.length);
                                    initRow(row, kv.value());
                                    return;
                                } else { // 否则,直接覆盖到缓存row中
                                    row.setField(field, getters[field].getFieldOrNull(kv.value()));
                                    updatedSequenceFields.add(field);
                                }
                            }
                        }
                    }
                    // (2) 回撤其他字段
                    else {
                        // retract normal field
                        // 若没有聚合器,则用null覆盖
                        if (aggregator == null) {
                            row.setField(i, null);
                        } else { // 若有聚合器,则调aggregator.retract()去进行回撤,底层如sum是做减法,max和min是没有回撤的,调FieldAggregator的会抛出异常,具体看FieldAggregator的子类
                            // retract agg field
                            Object accumulator = getters[i].getFieldOrNull(row);
                            row.setField(
                                    i,
                                    aggregator.retract(
                                            accumulator, getters[i].getFieldOrNull(kv.value())));
                        }
                    }
                }
                // CASE-3: 当前记录的序列 < 结果行的序列,但有聚合器,调aggregator.retract()去进行回撤
                else if (aggregator != null) {
                    // retract agg field for old sequence
                    Object accumulator = getters[i].getFieldOrNull(row);
                    row.setField(
                            i,
                            aggregator.retract(accumulator, getters[i].getFieldOrNull(kv.value())));
                }
            }
        }
    }

4.getResult()获取合并后的结果

java 复制代码
// 获取合并后的结果
@Override
public KeyValue getResult() {
    // 采用reused缓存kv实例,避免频繁创建对象
    if (reused == null) {
        reused = new KeyValue();
    }
    /* 根据两个状态判断最终的变更类型
        1. currentDeleteRow:标记 "当前记录是否需要被删除",在上面retractWithSequenceGroup中执行-D回撤的时候会赋true
        2. !meetInsert:标记 "当前记录是否不满足插入条件",重置是false,add会给true
    若currentDeleteRow为true 或 !meetInsert为true:生成RowKind.DELETE
     */
    RowKind rowKind = currentDeleteRow || !meetInsert ? RowKind.DELETE : RowKind.INSERT;
    return reused.replace(currentKey, latestSequenceNumber, rowKind, row); // 更新合并后的结果并返回
}

5.requireCopy()

不需要复制

java 复制代码
@Override
public boolean requireCopy() {
    return false;
}

三.总结

首先是参数配置

  1. private final boolean ignoreDelete; -> 由ignore-delete配置绑定;
  2. private final List<WrapperWithFieldIndex> fieldSeqComparators; -> 由sequnce-group配置绑定;
  3. private final boolean fieldSequenceEnabled; -> 上面参数fieldSeqComparators不为空,则它为true;
  4. private final List<WrapperWithFieldIndex> fieldAggregators; -> 由aggregate-function绑定;
  5. private final boolean removeRecordOnDelete; -> 由partial-update.remove-record-on-delete配置绑定;
  6. private final Set sequenceGroupPartialDelete; -> 由partial-update.remove-record-on-sequence-group配置绑定;

其次是add聚合流程 案例:

"fields.se_sign.sequence-group" : "uids,pids,views",

"fields.views.aggregate-function" : "sum",

  1. 若当前行kv数据的RowKind为-U/-D,则进行回撤操作;
  2. 若当前行kv数据的RowKind为+I/+U,则进行update聚合更新操作;
  3. 重点是配置sequnce-group的字段,分为标记字段和绑定字段
    • 标记字段:如se_sign,会进行原子性回撤/更新 -- 其实就是覆盖
    • 绑定字段:如uids、views,会进行回撤/更新
      • 非聚合的:回撤会直接给null;更新是直接覆盖
      • 聚合的:调聚合器的retract()回撤,如sum就是做减法;更新是直接调agg()聚合,如sum是做加法
相关推荐
枫叶梨花10 分钟前
一次 Kettle 中文乱码写入失败的完整排查实录
数据库·后端
申阳15 分钟前
Day 16:02. 基于 Tauri 2.0 开发后台管理系统-项目初始化配置
前端·后端·程序员
bcbnb16 分钟前
游戏上架 App Store 的完整发行流程,从构建、合规到审核的多角色协同指南
后端
JavaGuide17 分钟前
美团2026届后端一二面(附详细参考答案)
java·后端
aiopencode17 分钟前
无需源码的 iOS 加固方案 面向外包项目与存量应用的多层安全体系
后端
语落心生22 分钟前
Apache Geaflow推理框架Geaflow-infer 解析系列(六)共享内存架构
后端
语落心生25 分钟前
Apache Geaflow推理框架Geaflow-infer 解析系列(七)数据读写流程
后端
语落心生27 分钟前
Apache Geaflow推理框架Geaflow-infer 解析系列(五)环境上下文管理
后端
程序员爱钓鱼29 分钟前
用 Python 批量生成炫酷扫光 GIF 动效
后端·python·trae