Java 有没有一种有效的方法将几个(2个以上)卡夫卡主题连接起来?

Java 有没有一种有效的方法将几个(2个以上)卡夫卡主题连接起来?,java,stream,apache-kafka,outer-join,Java,Stream,Apache Kafka,Outer Join,我想按键连接几个(通常是2-10个)卡夫卡主题,最好使用流式API。所有主题都将具有相同的键和分区。执行此联接的一种方法是为每个主题创建一个KStream,并链接对KStream.outerJoin的调用: stream1 .outerJoin(stream2, ...) .outerJoin(stream3, ...) .outerJoin(stream4, ...) 但是,KStream.outerJoin的属性表明,对outerJoin的每次调用都将具体化其两个输

我想按键连接几个(通常是2-10个)卡夫卡主题,最好使用流式API。所有主题都将具有相同的键和分区。执行此联接的一种方法是为每个主题创建一个
KStream
,并链接对
KStream.outerJoin的调用:

stream1
    .outerJoin(stream2, ...)
    .outerJoin(stream3, ...)
    .outerJoin(stream4, ...)
但是,
KStream.outerJoin
的属性表明,对
outerJoin
的每次调用都将具体化其两个输入流,因此上面的示例不仅会具体化流1到4,而且还会具体化
stream1.outerJoin(stream2,…)
stream1.outerJoin(stream2,…).outerJoin(stream3,…)
。与直接连接4个流相比,将有许多不必要的序列化、反序列化和I/O

上述方法的另一个问题是,
JoinWindow
在所有4个输入流中不一致:一个
JoinWindow
将用于连接流1和2,但随后将使用单独的连接窗口连接此流和流3等,我为每个连接指定一个10秒的连接窗口,并且具有特定键的条目在0秒时出现在流1中,在6秒时出现在流2中,在12秒时出现在流3中,在18秒时出现在流4中,连接的项将在18秒后得到输出,从而导致延迟过高。结果取决于连接的顺序,这似乎不自然


是否有更好的方法使用Kafka进行多路连接?

最终,我决定创建一个自定义的轻量级连接程序,以避免具体化并严格遵守过期时间。平均应为O(1)。它比流API更适合消费者API:对于每个消费者,重复轮询并使用任何接收到的数据更新joiner;如果joiner返回一个完整的属性集,则将其转发。代码如下:

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

/**
 * Inner joins multiple streams of data by key into one stream. It is assumed
 * that a key will appear in a stream exactly once. The values associated with
 * each key are collected and if all values are received within a certain
 * maximum wait time, the joiner returns all values corresponding to that key.
 * If not all values are received in time, the joiner never returns any values
 * corresponding to that key.
 * <p>
 * This class is not thread safe: all calls to
 * {@link #update(Object, Object, long)} must be synchronized.
 * @param <K> The type of key.
 * @param <V> The type of value.
 */
class StreamInnerJoiner<K, V> {

    private final Map<K, Vals<V>> idToVals = new LinkedHashMap<>();
    private final int joinCount;
    private final long maxWait;

    /**
     * Creates a stream inner joiner.
     * @param joinCount The number of streams being joined.
     * @param maxWait The maximum amount of time after an item has been seen in
     * one stream to wait for it to be seen in the remaining streams.
     */
    StreamInnerJoiner(final int joinCount, final long maxWait) {
        this.joinCount = joinCount;
        this.maxWait = maxWait;
    }

    private static class Vals<A> {
        final long firstSeen;
        final Collection<A> vals = new ArrayList<>();
        private Vals(final long firstSeen) {
            this.firstSeen = firstSeen;
        }
    }

    /**
     * Updates this joiner with a value corresponding to a key.
     * @param key The key.
     * @param val The value.
     * @param now The current time.
     * @return If all values for the specified key have been received, the
     * complete collection of values for thaht key; otherwise
     * {@link Optional#empty()}.
     */
    Optional<Collection<V>> update(final K key, final V val, final long now) {
        expireOld(now - maxWait);
        final Vals<V> curVals = getOrCreate(key, now);
        curVals.vals.add(val);
        return expireAndGetIffFull(key, curVals);
    }

    private Vals<V> getOrCreate(final K key, final long now) {
        final Vals<V> existingVals = idToVals.get(key);
        if (existingVals != null)
            return existingVals;
        else {
            /*
            Note: we assume that the item with the specified ID has not already
            been seen and timed out, and therefore that its first seen time is
            now. If the item has in fact already timed out, it is doomed and
            will time out again with no ill effect.
             */
            final Vals<V> curVals = new Vals<>(now);
            idToVals.put(key, curVals);
            return curVals;
        }
    }

    private void expireOld(final long expireBefore) {
        final Iterator<Vals<V>> i = idToVals.values().iterator();
        while (i.hasNext() && i.next().firstSeen < expireBefore)
            i.remove();
    }

    private Optional<Collection<V>> expireAndGetIffFull(final K key, final Vals<V> vals) {
        if (vals.vals.size() == joinCount) {
            // as all expired entries were already removed, this entry is valid
            idToVals.remove(key);
            return Optional.of(vals.vals);
        } else
            return Optional.empty();
    }
}
import java.util.ArrayList;
导入java.util.Collection;
导入java.util.Iterator;
导入java.util.LinkedHashMap;
导入java.util.Map;
导入java.util.Optional;
/**
*内部通过键将多个数据流连接到一个流中。这是假定的
*密钥将在流中恰好出现一次。与关联的值
*收集每个键,如果在特定时间内收到所有值
*最大等待时间,joiner返回与该键对应的所有值。
*如果没有及时收到所有值,则joiner不会返回任何值
*对应于那个键。
*
*此类不是线程安全的:所有对
*{@link#update(Object,Object,long)}必须同步。
*@param键的类型。
*@param值的类型。
*/
类连接器{
私有最终映射idToVals=新LinkedHashMap();
私人最终整数计数;
私人最终长时间等待;
/**
*创建流内部joiner。
*@param joinCount正在加入的流的数量。
*@param maxWait在中看到项目后的最长时间
*一个流等待在其余流中看到它。
*/
StreamInnerJoiner(最终int joinCount、最终long maxWait){
this.joinCount=joinCount;
this.maxWait=maxWait;
}
专用静态类VAL{
最后的长首见;
最终收集VAL=新的ArrayList();
私人VAL(最终长首见){
this.firstSeen=firstSeen;
}
}
/**
*使用与键对应的值更新此联接程序。
*@param-key这个键。
*@param val值。
*@param现在是当前时间。
*@return如果已收到指定密钥的所有值,则
*完成该键的值集合;否则为
*{@link可选#empty()}。
*/
可选更新(最终K键、最终V值、最终long now){
expireOld(现在-maxWait);
最终VAL曲线=获取或创建(键,现在);
曲线val.add(val);
返回ExpireAndEventFull(关键帧、曲线);
}
私有VAL getOrCreate(最终K键,最终长键){
最终VAL EXISTINGVAL=IDTOVAL.get(键);
if(existingVals!=null)
返回现有值;
否则{
/*
注意:我们假设具有指定ID的项尚未
已被看到并超时,因此其第一次看到的时间是
现在。如果该项目实际上已经超时,那么它将注定失败
将再次超时,不会产生不良影响。
*/
最终VAL曲线=新VAL(现在);
idToVals.put(键、曲线);
返回曲线;
}
}
私有作废过期(最终长过期){
最终迭代器i=idToVals.values().Iterator();
while(i.hasNext()&&i.next().firstSeen
目前我不知道卡夫卡流中有更好的方法,但它正在酝酿中: