Подтвердить что ты не робот

Есть ли хороший способ извлечь куски данных из потока java 8?

Я процесс ETL Я извлекаю много объектов из репозитория данных Spring. Затем я использую параллельный поток для сопоставления объектов с разными. Я могу либо использовать пользователя для хранения этих новых объектов в другом репозитории один за другим, либо собирать их в список и хранить их в одной массовой операции. Первое дорого, в то время как более поздняя версия может превышать доступную память.

Есть ли хороший способ собрать определенное количество элементов в потоке (например, ограничение), использовать этот кусок и продолжать параллельно, пока все элементы не будут обработаны?

4b9b3361

Ответ 1

Мой подход к групповым операциям с chunking заключается в использовании разделителя разделителя разделов и другой оболочки, которая переопределяет политику расщепления по умолчанию (арифметическое продвижение размеров партии с шагом 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);
  }
}

Ответ 2

Возможно, вы сможете написать свой собственный Collector, который накапливает сущности, а затем выполняет массовые обновления.

Метод Collector.accumulator() может добавлять объекты во внутренний временный кеш, пока кеш не станет слишком большим. Когда кеш достаточно велик, вы можете сделать массовый магазин в другом репозитории.

Collector.merge() необходимо объединить 2 потока коллекционных кэшей в один кеш (и, возможно, слить)

Наконец, метод Collector.finisher() вызывается, когда поток выполняется, поэтому храните все, что осталось в кеше здесь.

Поскольку вы уже используете параллельный поток и, похоже, выполняете несколько нагрузок одновременно, я предполагаю, что вы уже справляетесь с потоковой безопасностью.

UPDATE

Мой комментарий относительно безопасности потоков и параллельных потоков относится к фактическому сохранению/хранению в репозитории, а не concurrency в вашей коллекции temp.

Каждый коллекционер должен (я думаю) работать в своем потоке. Параллельный поток должен создавать несколько экземпляров коллектора, вызывая supplier() несколько раз. Таким образом, вы можете рассматривать экземпляр коллектора как отдельный поток, и он должен работать нормально.

Например, в Javadoc для java.util.IntSummaryStatistics он говорит:

Эта реализация не является потокобезопасной. Тем не менее, безопасно использовать Collectors.toIntStatistics() для параллельного потока, поскольку параллельная реализация Stream.collect() обеспечивает необходимое разделение, изоляцию и объединение результатов для безопасного и эффективного параллельного выполнения.

Ответ 3

Вы можете использовать пользовательский коллекционер, чтобы сделать это элегантно.

Пожалуйста, см. мой ответ на аналогичный вопрос:

Сборщик пакетной обработки

Затем вы можете просто пакетно обрабатывать поток параллельно с помощью вышеуказанного сборщика для хранения записей в вашем репозитории, пример использования:

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));

Ответ 4

  @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

Ответ 5

Вот решение моей библиотеки: AbacusUtil:

stream.split(batchSize).parallel(threadNum).map(yourBatchProcessFunction);