Потік Java 8 з пакетною обробкою


95

У мене є великий файл, який містить список елементів.

Я хотів би створити партію елементів, зробити HTTP-запит з цією партією (усі елементи потрібні як параметри в HTTP-запиті). Я можу це зробити дуже легко за допомогою forциклу, але як любитель Java 8, я хочу спробувати написати це за допомогою фреймворку Stream Java 8 (і скористатися перевагами лінивої обробки).

Приклад:

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 # ліниво читатимуть вміст файлу.
Тобі

1
так що вам в основному потрібна обернена до flatMap(+ додаткова flatMap, щоб знову згорнути потоки)? Я не думаю, що щось подібне існує як зручний метод у стандартній бібліотеці. Або вам доведеться знайти сторонню бібліотеку, або написати свою власну на основі сплітераторів та / або колектора, що випромінює потік потоків
8472,

3
Можливо, ви можете поєднувати Stream.generateз reader::readLineі limit, але проблема в тому, що потоки не поєднуються з винятками. Крім того, це, мабуть, не вдається розпаралелювати добре. Я думаю, що forпетля все-таки є найкращим варіантом.
tobias_k

Я щойно додав приклад коду. Я не думаю, що flatMap - це шлях. Підозрюючи, що мені, можливо, доведеться написати власний Сплітератор
Енді Данг

1
Я формулюю термін "Потокове зловживання" для таких питань.
Кервін

Відповіді:


13

Примітка! Це рішення читає весь файл перед запуском forEach.

Ви можете зробити це за допомогою jOOλ , бібліотеки, яка розширює потоки Java 8 для однопотокових послідовних випадків використання:

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(на відміну, наприклад, від рішення Бен-Манеса)
Тагір Валєєв

124

Для повноти, ось рішення Гуави .

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

У питанні колекція доступна, тому потік не потрібен, і його можна записати як,

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

11
Lists.partitionце ще одна варіація, яку я повинен був би згадати.
Бен Манес

2
це ліниво, правда? він не буде викликати все Streamдо пам'яті перед обробкою відповідної партії
orirab,

1
@orirab так. Ліниво між партіями, оскільки в ньому буде витрачатися batchSizeелементів за ітерацію.
Бен Манес

Чи не могли б ви гляньте stackoverflow.com/questions/58666190 / ...
gstackoverflow

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
а якщо ваші дані насправді є потоком? (дозволяє вимовляти рядки у файлі або навіть з мережі).
Омрі Ядан

6
@OmryYadan, питання було про те , вхід від List(див data.size(), data.get()в цьому питанні). Я відповідаю на поставлене запитання. Якщо у вас є інше запитання, поставте його натомість (хоча я думаю, що запитання про трансляцію також уже було задане).
Тагір Валєєв 02

1
Як обробляти партії паралельно?
soup_boy

37

Чисте рішення Java 8 :

Ми можемо створити власний колектор, щоб зробити це елегантно, для обробки кожної партії використовуються символи a batch sizeта a Consumer:

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
Це хороше рішення, якщо ви не можете помістити всі елементи з вашого потоку в пам’ять. Крім того, він не буде працювати на нескінченних потоках - метод збирання є термінальним, що означає, що замість створення потоку пакетів він буде чекати, поки потік завершиться, а потім обробляти результат у партіях.
Алекс Акерман,

2
@AlexAckerman нескінченний потік означатиме, що фінішер ніколи не викликається, але акумулятор все одно буде викликаний, тому елементи все одно будуть оброблятися. Крім того, потрібно лише, щоб розмір партії елементів знаходився в пам'яті одночасно.
Solubris

@Solubris, ти маєш рацію! Шкода, дякую, що вказав на це - я не буду видаляти коментар для посилання, якщо хтось має таке саме уявлення про те, як працює метод збирання.
Алекс Акерман,

Список, надісланий споживачеві, слід скопіювати, щоб зробити його модифікацією безпечним, наприклад: batchProcessor.accept (copyOf (ts))
Solubris

19

Я написав спеціальний Spliterator для таких сценаріїв. Він заповнить списки заданого розміру із вхідного потоку. Перевага цього підходу полягає в тому, що він буде виконувати ледачу обробку і працюватиме з іншими потоковими функціями.

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-loop як умову (тоді цикл imho while стане більш читабельним)
pls

Я не впевнений, що імплементація правильна. Наприклад, якщо базовий потік - SUBSIZEDце спліти, з яких повертається, trySplitможе мати більше елементів, ніж до розбиття (якщо розбиття відбувається в середині пакета).
Солод

@Malt, якщо моє розуміння Spliteratorsправильне, то trySplitзавжди слід розділяти дані на дві приблизно рівні частини, щоб результат ніколи не був більшим за оригінал?
Брюс Гамільтон,

@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

Нам довелося вирішити подібну проблему. Ми хотіли взяти потік, який перевищував системну пам’ять (перебираючи всі об’єкти бази даних), і якнайкраще рандомізувати порядок - ми вважали, що було б добре буферувати 10000 елементів і рандомізувати їх.

Ціль була функцією, яка брала потік.

З запропонованих тут рішень існує цілий ряд варіантів:

  • Використовуйте різні додаткові бібліотеки без Java
  • Почніть з чогось, що не є потоком, наприклад, зі списку довільного доступу
  • Майте потік, який можна легко розщепити в сплітераторі

Спочатку наш інстинкт полягав у використанні спеціального колектора, але це означало відмову від потокового передавання. Наведене вище спеціальне колекторне рішення дуже хороше, і ми його майже використали.

Ось рішення, яке обманює, використовуючи той факт, що Streams може дати вам рішення, Iteratorяке ви можете використовувати як евакуаційний люк щоб дозволити вам зробити щось додаткове, що потоки не підтримують. IteratorПеретвориться назад в потік з використанням іншого трохи Java 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ви не можете відкласти ітерацію елементів всередині партії, оскільки споживач може захотіти пропустити всю партію, і якщо ви не спожили тоді вони не пропускали б дуже далеко. (Я впровадив один із них у 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

Ви також можете поглянути на циклоп-реакцію , я є автором цієї бібліотеки. Він реалізує інтерфейс jOOλ (і за розширенням JDK 8 Streams), але на відміну від JDK 8 Parallel Streams він фокусується на асинхронних операціях (таких як потенційно блокування викликів вводу-виводу Async). Паралельні потоки JDK, навпаки, фокусуються на паралельності даних для операцій, пов'язаних з процесором. Він працює, керуючи сукупностями завдань на основі майбутнього, але представляє стандартний розширений Stream API для кінцевих користувачів.

Цей зразок коду може допомогти вам розпочати роботу

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

Тут є підручник з дозування

І більш загальний підручник тут

Щоб використовувати власний пул потоків (який, мабуть, більше підходить для блокування вводу-виводу), ви можете розпочати обробку з

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

Частина розділу виконується не ліниво, але після того, як список розділений, ви отримуєте переваги роботи з потоками (наприклад, використовуйте паралельні потоки, додайте фільтри тощо). Інші відповіді пропонували більш складні рішення, але інколи читабельність та ремонтопридатність важливіші (а іноді і ні :-))


Не впевнений, хто голосував за, але було б приємно зрозуміти, чому .. Я дав відповідь, яка доповнила інші відповіді для людей, які не можуть користуватися гуавою
Тал Джоффе

Ви обробляєте тут список, а не потік.
Дракемор

@Drakemor Я обробляю потік під-списків. зверніть увагу на виклик функції stream ()
Таль Йоффе

Але спочатку ви перетворюєте його на список підсписків, який не буде коректно працювати для справжніх потокових даних. Ось посилання на розділ: commons.apache.org/proper/commons-collections/apidocs/org/…
Дракемор,

1
TBH Я не повністю розумію ваші аргументи, але, мабуть, ми можемо погодитися не погодитися. Я відредагував свою відповідь, щоб відобразити нашу розмову тут. Дякую за обговорення
Тал Джоффе

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)
);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.