バッチ処理付きのJava 8ストリーム


95

アイテムのリストを含む大きなファイルがあります。

アイテムのバッチを作成し、このバッチでHTTPリクエストを作成します(すべてのアイテムがHTTPリクエストのパラメーターとして必要です)。私はforループを使用して非常に簡単にそれを行うことができますが、Java 8の愛好家として、Java 8のStreamフレームワークでこれを書いてみてください(そして、遅延処理の利点を享受します)。

例:

List<String> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < data.size(); i++) {
  batch.add(data.get(i));
  if (batch.size() == BATCH_SIZE) process(batch);
}

if (batch.size() > 0) process(batch);

長いことやりたい lazyFileStream.group(500).map(processBatch).collect(toList())

これを行う最善の方法は何でしょうか?


グループ化の実行方法がよくわかりませんが、Files#linesファイルの内容を遅延して読み取ります。
Toby

1
したがって、基本的にはflatMap(+ストリームを再度折りたたむための追加のflatMap)の逆が必要ですか?そのようなものは標準ライブラリの便利なメソッドとしては存在しないと思います。サードパーティのライブラリを見つけるか、スプリッタやストリームのストリームを放出するコレクタに基づいて独自のライブラリを作成する必要があります
the8472

3
たぶん、あなたは組み合わせることができStream.generatereader::readLinelimit、しかし問題はストリームは例外とうまく行っていないということです。また、これはおそらくうまく並列化できません。forループはまだ最良の選択肢だと思います。
tobias_k

サンプルコードを追加しました。私はflatMapが進むべき道だとは思いません。カスタムのスプリッターを作成する必要があるのではないかと疑う
Andy Dang

1
私はこのような質問に対して「ストリームの乱用」という言葉を作り出しました。
カービン

回答:


13

注意!このソリューションは、forEachを実行する前にファイル全体を読み取ります。

シングルスレッドのシーケンシャルストリームのユースケース用にJava 8ストリームを拡張するライブラリであるjOOλでそれを行うことができます。

Seq.seq(lazyFileStream)              // Seq<String>
   .zipWithIndex()                   // Seq<Tuple2<String, Long>>
   .groupBy(tuple -> tuple.v2 / 500) // Map<Long, List<String>>
   .forEach((index, batch) -> {
       process(batch);
   });

舞台裏でzipWithIndex()は、ただです:

static <T> Seq<Tuple2<T, Long>> zipWithIndex(Stream<T> stream) {
    final Iterator<T> it = stream.iterator();

    class ZipWithIndex implements Iterator<Tuple2<T, Long>> {
        long index;

        @Override
        public boolean hasNext() {
            return it.hasNext();
        }

        @Override
        public Tuple2<T, Long> next() {
            return tuple(it.next(), index++);
        }
    }

    return seq(new ZipWithIndex());
}

...一方、groupBy()APIは次の場合に便利です。

default <K> Map<K, List<T>> groupBy(Function<? super T, ? extends K> classifier) {
    return collect(Collectors.groupingBy(classifier));
}

(免責事項:私はjOOλの背後にある会社で働いています)


ワオ。これはまさに私が探しているものです。これは、Java 8に移動するには良いフィット感になりので、私たちのシステムは正常に処理データを順番にストリーム
アンディ・ダン

16
このソリューションでは、入力ストリーム全体が中間に不必要に格納されることに注意してくださいMap(たとえば、Ben Manesソリューションとは異なります)
Tagir Valeev

124

完全を期すために、ここにGuavaソリューションを示します。

Iterators.partition(stream.iterator(), batchSize).forEachRemaining(this::process);

質問では、コレクションが利用できるので、ストリームは必要なく、次のように書くことができます。

Iterables.partition(data, batchSize).forEach(this::process);

11
Lists.partition私が言及すべきもう一つのバリエーションです。
Ben Manes

2
これは怠惰ですよね?Stream関連するバッチを処理する前に全体をメモリに呼び出しません
orirab

1
@orirabはい。batchSize反復ごとに要素を消費するため、バッチ間で遅延します。
Ben Manes


58

純粋なJava-8実装も可能です。

int BATCH = 500;
IntStream.range(0, (data.size()+BATCH-1)/BATCH)
         .mapToObj(i -> data.subList(i*BATCH, Math.min(data.size(), (i+1)*BATCH)))
         .forEach(batch -> process(batch));

JOOlとは異なり、並行してうまく機能することに注意してください(dataランダムアクセスリストの場合)。


1
データが実際にストリームの場合はどうなりますか?(ファイル内の行、またはネットワークからの行も言います)。
Omry Yadan 2016

6
@OmryYadan、質問はList(質問のを参照data.size())からの入力に関するものでしたdata.get()。私は尋ねられた質問に答えています。別の質問がある場合は、代わりに質問してください(ただし、ストリームの質問も既に質問されていると思います)。
Tagir Valeev、2016

1
バッチを並行して処理する方法は?
soup_boy

37

純粋なJava 8ソリューション

カスタムコレクターを作成して、これをエレガントに行うことができます。これにより、各バッチを処理batch sizeするConsumerためにa とa を受け取ります。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collector;

import static java.util.Objects.requireNonNull;


/**
 * Collects elements in the stream and calls the supplied batch processor
 * after the configured batch size is reached.
 *
 * In case of a parallel stream, the batch processor may be called with
 * elements less than the batch size.
 *
 * The elements are not kept in memory, and the final result will be an
 * empty list.
 *
 * @param <T> Type of the elements being collected
 */
class BatchCollector<T> implements Collector<T, List<T>, List<T>> {

    private final int batchSize;
    private final Consumer<List<T>> batchProcessor;


    /**
     * Constructs the batch collector
     *
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     */
    BatchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        batchProcessor = requireNonNull(batchProcessor);

        this.batchSize = batchSize;
        this.batchProcessor = batchProcessor;
    }

    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    public BiConsumer<List<T>, T> accumulator() {
        return (ts, t) -> {
            ts.add(t);
            if (ts.size() >= batchSize) {
                batchProcessor.accept(ts);
                ts.clear();
            }
        };
    }

    public BinaryOperator<List<T>> combiner() {
        return (ts, ots) -> {
            // process each parallel list without checking for batch size
            // avoids adding all elements of one to another
            // can be modified if a strict batching mode is required
            batchProcessor.accept(ts);
            batchProcessor.accept(ots);
            return Collections.emptyList();
        };
    }

    public Function<List<T>, List<T>> finisher() {
        return ts -> {
            batchProcessor.accept(ts);
            return Collections.emptyList();
        };
    }

    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

オプションで、ヘルパーユーティリティクラスを作成します。

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collector;

public class StreamUtils {

    /**
     * Creates a new batch collector
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     * @param <T> the type of elements being processed
     * @return a batch collector instance
     */
    public static <T> Collector<T, List<T>, List<T>> batchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        return new BatchCollector<T>(batchSize, batchProcessor);
    }
}

使用例:

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> output = new ArrayList<>();

int batchSize = 3;
Consumer<List<Integer>> batchProcessor = xs -> output.addAll(xs);

input.stream()
     .collect(StreamUtils.batchCollector(batchSize, batchProcessor));

コードをGitHubにも投稿しました。

Githubへのリンク


1
これは、ストリームのすべての要素をメモリに収めることができない場合を除き、良い解決策です。また、無限ストリームでは機能しません-collectメソッドはターミナルです。つまり、バッチのストリームを生成するのではなく、ストリームが完了するまで待機してから、結果をバッチで処理します。
Alex Ackerman

2
@AlexAckerman無限ストリームは、フィニッシャーが呼び出されないことを意味しますが、アキュムレーターは引き続き呼び出されるため、アイテムは引き続き処理されます。また、一度にメモリに存在する必要があるのは、アイテムのバッチサイズのみです。
Solubris

@Solubris、あなたは正しいです!これを指摘してくれてありがとう。collectメソッドの仕組みについて誰かが同じ考えを持っている場合は、参照用のコメントを削除しません。
Alex Ackerman

コンシューマーに送信されたリストは、変更を安全にするためにコピーする必要があります。例:batchProcessor.accept(copyOf(ts))
Solubris

19

このようなシナリオ用にカスタムのスプリッターを作成しました。入力ストリームから指定されたサイズのリストを埋めます。このアプローチの利点は、遅延処理を実行し、他のストリーム関数で機能することです。

public static <T> Stream<List<T>> batches(Stream<T> stream, int batchSize) {
    return batchSize <= 0
        ? Stream.of(stream.collect(Collectors.toList()))
        : StreamSupport.stream(new BatchSpliterator<>(stream.spliterator(), batchSize), stream.isParallel());
}

private static class BatchSpliterator<E> implements Spliterator<List<E>> {

    private final Spliterator<E> base;
    private final int batchSize;

    public BatchSpliterator(Spliterator<E> base, int batchSize) {
        this.base = base;
        this.batchSize = batchSize;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<E>> action) {
        final List<E> batch = new ArrayList<>(batchSize);
        for (int i=0; i < batchSize && base.tryAdvance(batch::add); i++)
            ;
        if (batch.isEmpty())
            return false;
        action.accept(batch);
        return true;
    }

    @Override
    public Spliterator<List<E>> trySplit() {
        if (base.estimateSize() <= batchSize)
            return null;
        final Spliterator<E> splitBase = this.base.trySplit();
        return splitBase == null ? null
                : new BatchSpliterator<>(splitBase, batchSize);
    }

    @Override
    public long estimateSize() {
        final double baseSize = base.estimateSize();
        return baseSize == 0 ? 0
                : (long) Math.ceil(baseSize / (double) batchSize);
    }

    @Override
    public int characteristics() {
        return base.characteristics();
    }

}

本当に役に立ちました。誰かがいくつかのカスタム基準(たとえば、バイト単位のコレクションのサイズ)でバッチ処理する場合は、カスタム述語を委任して、それを条件としてforループで使用できます(whihループがより読みやすくなります)
pls

実装が正しいかどうかはわかりません。たとえば、ベースストリームがSUBSIZEDスプリットである場合、返されるスプリットは、スプリットtrySplit前よりも多くのアイテムを持つことができます(スプリットがバッチの途中で発生した場合)。
モルト

@Malt私の理解Spliteratorsが正しいtrySplit場合は、データを常にほぼ等しい2つの部分に分割し、結果が元のサイズより大きくならないようにしますか?
ブルースハミルトン

@BruceHamilton残念ながら、ドキュメントによると、パーツをほぼ同じにすることはできません。彼らは必要があります等しいこと:if this Spliterator is SUBSIZED, then estimateSize() for this spliterator before splitting must be equal to the sum of estimateSize() for this and the returned Spliterator after splitting.
モルト

はい、それはSpliterator分割に関する私の理解と一致しています。しかし、「trySplitから返された分割が分割前よりも多くのアイテムを持つことができる」ことを理解するのに苦労しています。そこでの意味を詳しく説明してもらえますか?
ブルースハミルトン

13

解決すべき同様の問題がありました。システムメモリよりも大きいストリームを取得し(データベース内のすべてのオブジェクトを反復処理)、順序を可能な限り最適化しました。10,000アイテムをバッファリングし、それらをランダム化することは問題ないと考えました。

ターゲットは、ストリームを取り込む関数でした。

ここで提案されているソリューションには、さまざまな選択肢があるようです。

  • Java 8以外のさまざまな追加ライブラリを使用する
  • ストリームではないものから始めます-ランダムアクセスリストなど
  • スプリッターで簡単に分割できるストリームを持っている

私たちの本能はもともとカスタムコレクターを使用することでしたが、これはストリーミングを中止することを意味しました。上記のカスタムコレクターソリューションは非常に優れており、ほぼ使用しました。

以下は、StreamsがエスケープハッチIteratorとして使用できるを提供しストリームがサポートしていない追加の処理を実行できるという事実を利用して、不正を行うソリューションです。Iteratorジャワ8の別のビット使用してストリームに変換されるStreamSupport魔術を。

/**
 * An iterator which returns batches of items taken from another iterator
 */
public class BatchingIterator<T> implements Iterator<List<T>> {
    /**
     * Given a stream, convert it to a stream of batches no greater than the
     * batchSize.
     * @param originalStream to convert
     * @param batchSize maximum size of a batch
     * @param <T> type of items in the stream
     * @return a stream of batches taken sequentially from the original stream
     */
    public static <T> Stream<List<T>> batchedStreamOf(Stream<T> originalStream, int batchSize) {
        return asStream(new BatchingIterator<>(originalStream.iterator(), batchSize));
    }

    private static <T> Stream<T> asStream(Iterator<T> iterator) {
        return StreamSupport.stream(
            Spliterators.spliteratorUnknownSize(iterator,ORDERED),
            false);
    }

    private int batchSize;
    private List<T> currentBatch;
    private Iterator<T> sourceIterator;

    public BatchingIterator(Iterator<T> sourceIterator, int batchSize) {
        this.batchSize = batchSize;
        this.sourceIterator = sourceIterator;
    }

    @Override
    public boolean hasNext() {
        prepareNextBatch();
        return currentBatch!=null && !currentBatch.isEmpty();
    }

    @Override
    public List<T> next() {
        return currentBatch;
    }

    private void prepareNextBatch() {
        currentBatch = new ArrayList<>(batchSize);
        while (sourceIterator.hasNext() && currentBatch.size() < batchSize) {
            currentBatch.add(sourceIterator.next());
        }
    }
}

これを使用する簡単な例は次のようになります。

@Test
public void getsBatches() {
    BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
        .forEach(System.out::println);
}

上記のプリント

[A, B, C]
[D, E, F]

私たちの使用例では、バッチをシャッフルしてからストリームとして保持したかったのですが、次のようになります。

@Test
public void howScramblingCouldBeDone() {
    BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
        // the lambda in the map expression sucks a bit because Collections.shuffle acts on the list, rather than returning a shuffled one
        .map(list -> {
            Collections.shuffle(list); return list; })
        .flatMap(List::stream)
        .forEach(System.out::println);
}

これは次のようなものを出力します(ランダム化されているため、毎回異なります)

A
C
B
E
D
F

ここでの秘訣は、常にストリームが存在することです。したがって、バッチのストリームを操作するか、各バッチに対して何かを実行flatMapしてから、ストリームに戻すことができます。さらに良いことに、上記のすべてが最終forEachまたはcollect、または他の終端式PULLストリームを介してデータを。

これは、ストリームに対するiterator特別な種類の終了操作であり、ストリーム全体が実行されてメモリに読み込まれるわけではありません。素晴らしいデザインをしてくれたJava 8の皆さんに感謝します!


そして、収集されたときに各バッチを完全に反復し、それまで継続することは非常に良いことです。Listコンシューマがバッチ全体をスキップする可能性があるため、バッチ内要素の反復を延期することはできません。要素は、それほど遠くまでスキップしません。(私はこれらの1つをC#に実装しましたが、大幅に簡単になりました。)
ErikE

9

RxJavaを使用することもできます

Observable.from(data).buffer(BATCH_SIZE).forEach((batch) -> process(batch));

または

Observable.from(lazyFileStream).buffer(500).map((batch) -> process(batch)).toList();

または

Observable.from(lazyFileStream).buffer(500).map(MyClass::process).toList();

8

また、cyclops-reactを確認することもできます。。私はこのライブラリの作成者です。jOOλインターフェース(および拡張JDK 8ストリーム)を実装しますが、JDK 8パラレルストリームとは異なり、非同期操作(非同期I / O呼び出しをブロックする可能性など)に重点を置いています。対照的に、JDK Parallel Streamsは、CPUバインド操作のデータ並列処理に焦点を当てています。これは、Futureベースのタスクの集合体を内部で管理することによって機能しますが、標準の拡張Stream APIをエンドユーザーに提供します。

このサンプルコードはあなたが始めるのに役立つかもしれません

LazyFutureStream.parallelCommonBuilder()
                .react(data)
                .grouped(BATCH_SIZE)                  
                .map(this::process)
                .run();

ここにバッチ処理に関するチュートリアルがあります

そして より一般的なチュートリアルはこちら

独自のスレッドプールを使用するには(おそらくI / Oのブロックに適しています)、次のコマンドで処理を開始できます。

     LazyReact reactor = new LazyReact(40);

     reactor.react(data)
            .grouped(BATCH_SIZE)                  
            .map(this::process)
            .run();

3

並列ストリームでも動作する純粋なJava 8の例。

使い方:

Stream<Integer> integerStream = IntStream.range(0, 45).parallel().boxed();
CsStreamUtil.processInBatch(integerStream, 10, batch -> System.out.println("Batch: " + batch));

メソッドの宣言と実装:

public static <ElementType> void processInBatch(Stream<ElementType> stream, int batchSize, Consumer<Collection<ElementType>> batchProcessor)
{
    List<ElementType> newBatch = new ArrayList<>(batchSize);

    stream.forEach(element -> {
        List<ElementType> fullBatch;

        synchronized (newBatch)
        {
            if (newBatch.size() < batchSize)
            {
                newBatch.add(element);
                return;
            }
            else
            {
                fullBatch = new ArrayList<>(newBatch);
                newBatch.clear();
                newBatch.add(element);
            }
        }

        batchProcessor.accept(fullBatch);
    });

    if (newBatch.size() > 0)
        batchProcessor.accept(new ArrayList<>(newBatch));
}

2

公平を期して、エレガントなVavrソリューションを見てください。

Stream.ofAll(data).grouped(BATCH_SIZE).forEach(this::process);

1

Spliteratorを使用した簡単な例

    // read file into stream, try-with-resources
    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        //skip header
        Spliterator<String> split = stream.skip(1).spliterator();
        Chunker<String> chunker = new Chunker<String>();
        while(true) {              
            boolean more = split.tryAdvance(chunker::doSomething);
            if (!more) {
                break;
            }
        }           
    } catch (IOException e) {
        e.printStackTrace();
    }
}

static class Chunker<T> {
    int ct = 0;
    public void doSomething(T line) {
        System.out.println(ct++ + " " + line.toString());
        if (ct % 100 == 0) {
            System.out.println("====================chunk=====================");               
        }           
    }       
}

ブルースの答えはより包括的ですが、私は一連のファイルを処理するために迅速で汚いものを探していました。


1

これは遅延評価される純粋なJavaソリューションです。

public static <T> Stream<List<T>> partition(Stream<T> stream, int batchSize){
    List<List<T>> currentBatch = new ArrayList<List<T>>(); //just to make it mutable 
    currentBatch.add(new ArrayList<T>(batchSize));
    return Stream.concat(stream
      .sequential()                   
      .map(new Function<T, List<T>>(){
          public List<T> apply(T t){
              currentBatch.get(0).add(t);
              return currentBatch.get(0).size() == batchSize ? currentBatch.set(0,new ArrayList<>(batchSize)): null;
            }
      }), Stream.generate(()->currentBatch.get(0).isEmpty()?null:currentBatch.get(0))
                .limit(1)
    ).filter(Objects::nonNull);
}

1

あなたはapache.commonsを使うことができます:

ListUtils.partition(ListOfLines, 500).stream()
                .map(partition -> processBatch(partition)
                .collect(Collectors.toList());

パーティショニングの部分はゆるやかに行われますが、リストがパーティショニングされた後は、ストリームを操作するメリットが得られます(たとえば、並列ストリームの使用、フィルターの追加など)。他の回答はより複雑な解決策を提案しましたが、時々読みやすさと保守性がより重要です(そして時々それらは:-)ではありません)


誰が反対票を投じたのかわからないが、その理由を理解するのはいいだろう。グアバを使用できない人々のための他の回答を補足する回答を与えた
Tal Joffe

ストリームではなく、ここでリストを処理しています。
Drakemor

@Drakemorサブリストのストリームを処理しています。stream()関数の呼び出しに注意してください
Tal Joffe

ただし、最初にそれをサブリストのリストに変換します。これは、実際のストリーミングデータに対しては正しく機能しません。ここでは、パーティションへの参照は次のとおりです。 commons.apache.org/proper/commons-collections/apidocs/org/...
Drakemor

1
TBH私はあなたの議論を完全には理解していませんが、私は反対することに同意できると思います。ここでの会話を反映するように回答を編集しました。ディスカッションをありがとう
Tal Joffe

1

Reactorを使用して簡単に実行できます 。

Flux.fromStream(fileReader.lines().onClose(() -> safeClose(fileReader)))
            .map(line -> someProcessingOfSingleLine(line))
            .buffer(BUFFER_SIZE)
            .subscribe(apiService::makeHttpRequest);

0

Java 8およびを使用するとcom.google.common.collect.Lists、次のようなことができます。

public class BatchProcessingUtil {
    public static <T,U> List<U> process(List<T> data, int batchSize, Function<List<T>, List<U>> processFunction) {
        List<List<T>> batches = Lists.partition(data, batchSize);
        return batches.stream()
                .map(processFunction) // Send each batch to the process function
                .flatMap(Collection::stream) // flat results to gather them in 1 stream
                .collect(Collectors.toList());
    }
}

ここにTは、入力リストUのアイテムのタイプと出力リストのアイテムのタイプがあります

そして、あなたはそれをこのように使うことができます:

List<String> userKeys = [... list of user keys]
List<Users> users = BatchProcessingUtil.process(
    userKeys,
    10, // Batch Size
    partialKeys -> service.getUsers(partialKeys)
);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.