Spring Data Repositoryから多くのエンティティを取得しているETLプロセスです。次に、並列ストリームを使用して、エンティティを別のエンティティにマップします。コンシューマーを使用して、これらの新しいエンティティを1つずつ別のリポジトリに保存するか、それらをリストに収集して、1回の一括操作で保存できます。前者はコストがかかり、後者は使用可能なメモリを超える可能性があります。
(limitのように)ストリーム内の特定の量の要素を収集し、そのチャンクを消費し、すべての要素が処理されるまで並行して続行する良い方法はありますか?
チャンクによる一括操作への私のアプローチは、パーティション分割スプリッターラッパーと、デフォルトの分割ポリシー(1024単位の増分でのバッチサイズの算術的な進行)を単純な固定バッチ分割にオーバーライドする別のラッパーを使用することです。次のように使用します。
Stream<OriginalType> existingStream = ...;
Stream<List<OriginalType>> partitioned = partition(existingStream, 100, 1);
partitioned.forEach(chunk -> ... process the chunk ...);
ここに完全なコードがあります:
import Java.util.ArrayList;
import Java.util.List;
import Java.util.Spliterator;
import Java.util.Spliterators.AbstractSpliterator;
import Java.util.function.Consumer;
import Java.util.stream.Stream;
import Java.util.stream.StreamSupport;
public class PartitioningSpliterator<E> extends AbstractSpliterator<List<E>>
{
private final Spliterator<E> spliterator;
private final int partitionSize;
public PartitioningSpliterator(Spliterator<E> toWrap, int partitionSize) {
super(toWrap.estimateSize(), toWrap.characteristics() | Spliterator.NONNULL);
if (partitionSize <= 0) throw new IllegalArgumentException(
"Partition size must be positive, but was " + partitionSize);
this.spliterator = toWrap;
this.partitionSize = partitionSize;
}
public static <E> Stream<List<E>> partition(Stream<E> in, int size) {
return StreamSupport.stream(new PartitioningSpliterator(in.spliterator(), size), false);
}
public static <E> Stream<List<E>> partition(Stream<E> in, int size, int batchSize) {
return StreamSupport.stream(
new FixedBatchSpliterator<>(new PartitioningSpliterator<>(in.spliterator(), size), batchSize), false);
}
@Override public boolean tryAdvance(Consumer<? super List<E>> action) {
final ArrayList<E> partition = new ArrayList<>(partitionSize);
while (spliterator.tryAdvance(partition::add)
&& partition.size() < partitionSize);
if (partition.isEmpty()) return false;
action.accept(partition);
return true;
}
@Override public long estimateSize() {
final long est = spliterator.estimateSize();
return est == Long.MAX_VALUE? est
: est / partitionSize + (est % partitionSize > 0? 1 : 0);
}
}
import static Java.util.Spliterators.spliterator;
import Java.util.Comparator;
import Java.util.Spliterator;
import Java.util.function.Consumer;
public abstract class FixedBatchSpliteratorBase<T> implements Spliterator<T> {
private final int batchSize;
private final int characteristics;
private long est;
public FixedBatchSpliteratorBase(int characteristics, int batchSize, long est) {
characteristics |= ORDERED;
if ((characteristics & SIZED) != 0) characteristics |= SUBSIZED;
this.characteristics = characteristics;
this.batchSize = batchSize;
this.est = est;
}
public FixedBatchSpliteratorBase(int characteristics, int batchSize) {
this(characteristics, batchSize, Long.MAX_VALUE);
}
public FixedBatchSpliteratorBase(int characteristics) {
this(characteristics, 64, Long.MAX_VALUE);
}
@Override public Spliterator<T> trySplit() {
final HoldingConsumer<T> holder = new HoldingConsumer<>();
if (!tryAdvance(holder)) return null;
final Object[] a = new Object[batchSize];
int j = 0;
do a[j] = holder.value; while (++j < batchSize && tryAdvance(holder));
if (est != Long.MAX_VALUE) est -= j;
return spliterator(a, 0, j, characteristics());
}
@Override public Comparator<? super T> getComparator() {
if (hasCharacteristics(SORTED)) return null;
throw new IllegalStateException();
}
@Override public long estimateSize() { return est; }
@Override public int characteristics() { return characteristics; }
static final class HoldingConsumer<T> implements Consumer<T> {
Object value;
@Override public void accept(T value) { this.value = value; }
}
}
import static Java.util.stream.StreamSupport.stream;
import Java.util.Spliterator;
import Java.util.function.Consumer;
import Java.util.stream.Stream;
public class FixedBatchSpliterator<T> extends FixedBatchSpliteratorBase<T> {
private final Spliterator<T> spliterator;
public FixedBatchSpliterator(Spliterator<T> toWrap, int batchSize, long est) {
super(toWrap.characteristics(), batchSize, est);
this.spliterator = toWrap;
}
public FixedBatchSpliterator(Spliterator<T> toWrap, int batchSize) {
this(toWrap, batchSize, toWrap.estimateSize());
}
public FixedBatchSpliterator(Spliterator<T> toWrap) {
this(toWrap, 64, toWrap.estimateSize());
}
public static <T> Stream<T> withBatchSize(Stream<T> in, int batchSize) {
return stream(new FixedBatchSpliterator<>(in.spliterator(), batchSize), true);
}
public static <T> FixedBatchSpliterator<T> batchedSpliterator(Spliterator<T> toWrap, int batchSize) {
return new FixedBatchSpliterator<>(toWrap, batchSize);
}
@Override public boolean tryAdvance(Consumer<? super T> action) {
return spliterator.tryAdvance(action);
}
@Override public void forEachRemaining(Consumer<? super T> action) {
spliterator.forEachRemaining(action);
}
}
エンティティを累積して一括更新を実行する独自のCollector
を作成できる場合があります。
Collector.accumulator()
メソッドは、キャッシュが大きくなりすぎるまで、エンティティを内部一時キャッシュに追加できます。キャッシュが十分に大きい場合、他のリポジトリに一括保存できます。
Collector.merge()
は、2つのスレッドのコレクターのキャッシュを1つのキャッシュに結合する必要があります(場合によってはマージする)
最後に、ストリームが終了するとCollector.finisher()
メソッドが呼び出されるため、キャッシュに残っているものもここに保存します。
すでに並列ストリームを使用していて、同時に複数のロードを実行しても問題ないように見えるので、スレッドセーフはすでに処理されていると思います。
[〜#〜]更新[〜#〜]
スレッドセーフティと並列ストリームに関する私のコメントは、一時的なコレクションでの同時実行性ではなく、リポジトリへの実際の保存/保存に関するものでした。
各コレクターは(私が思うに)独自のスレッドで実行する必要があります。並列ストリームは、supplier()
を複数回呼び出すことにより、複数のコレクターインスタンスを作成する必要があります。したがって、コレクタインスタンスをシングルスレッドとして扱うことができ、正常に動作するはずです。
たとえば、Java.util.IntSummaryStatistics
のJavadocでは次のように記述されています。
この実装はスレッドセーフではありません。ただし、Stream.collect()の並列実装により、安全で効率的な並列実行に必要な結果の分割、分離、およびマージが提供されるため、並列ストリームでCollectors.toIntStatistics()を使用しても安全です。
カスタムコレクターを使用して、これをエレガントに行うことができます。
ここで同様の質問に対する私の答えを見てください:
次に、上記のコレクターを使用してストリームを並行してバッチ処理するだけで、レコードをリポジトリに保存できます(使用例)。
List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int batchSize = 3;
Consumer<List<Integer>> batchProcessor = xs -> repository.save(xs);
input.parallelStream()
.map(i -> i + 1)
.collect(StreamUtils.batchCollector(batchSize, batchProcessor));
@Test
public void streamTest(){
Stream<Integer> data = Stream.generate(() -> {
//Block on IO
return blockOnIO();
});
AtomicInteger countDown = new AtomicInteger(1000);
final ArrayList[] buffer = new ArrayList[]{new ArrayList<Integer>()};
Object syncO = new Object();
data.parallel().unordered().map(i -> i * 1000).forEach(i->{
System.out.println(String.format("FE %s %d",Thread.currentThread().getName(), buffer[0].size()));
int c;
ArrayList<Integer> export=null;
synchronized (syncO) {
c = countDown.addAndGet(-1);
buffer[0].add(i);
if (c == 0) {
export=buffer[0];
buffer[0] = new ArrayList<Integer>();
countDown.set(1000);
}
}
if(export !=null){
sendBatch(export);
}
});
//export any remaining
sendBatch(buffer[0]);
}
Integer blockOnIO(){
try {
Thread.sleep(50);
return Integer.valueOf((int)Math.random()*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
void sendBatch(ArrayList al){
assert al.size() == 1000;
System.out.println(String.format("LOAD %s %d",Thread.currentThread().getName(), al.size()));
}
これは多分古い方法ですが、最小限のロックでバッチ処理を実現する必要があります。
出力は次のようになります
FE ForkJoinPool.commonPool-worker-2 996
FE ForkJoinPool.commonPool-worker-5 996
FE ForkJoinPool.commonPool-worker-4 998
FE ForkJoinPool.commonPool-worker-3 999
LOAD ForkJoinPool.commonPool-worker-3 1000
FE ForkJoinPool.commonPool-worker-6 0
FE ForkJoinPool.commonPool-worker-1 2
FE ForkJoinPool.commonPool-worker-7 2
FE ForkJoinPool.commonPool-worker-2 4
これが私のライブラリによる解決策です: AbacusUtil :
stream.split(batchSize).parallel(threadNum).map(yourBatchProcessFunction);