Spring Batch с использованием Partitioner

Spring Batch с использованием Partitioner

1. обзор

В наших предыдущихintroduction to Spring Batch мы представили фреймворк как инструмент пакетной обработки. Мы также изучили детали конфигурации и реализацию однопоточного, однопроцессного выполнения задания.

Для реализации задания с некоторой параллельной обработкой предоставляется ряд опций. На более высоком уровне есть два режима параллельной обработки:

  1. Однопроцессный, многопоточный

  2. Мульти-процесс

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

2. Разбиение шага

Spring Batch с разделением предоставляет нам возможность разделить выполнениеStep:

image

На рисунке выше показана реализацияJob с разделеннымStep.

ЕстьStep, называемый «Master», выполнение которого разделено на несколько шагов «Slave». Эти рабы могут занять место хозяина, и результат все равно останется неизменным. И ведущий, и ведомый являются экземплярамиStep. Подчиненные устройства могут быть удаленными службами или просто локально выполняемыми потоками.

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

Вот схема последовательности, показывающая, как все это работает:

image

Как показано,PartitionStep управляет выполнением. PartitionHandler отвечает за разделение работы «мастера» на «подчиненных». Самый правыйStep - подчиненный.

3. Maven POM

Зависимости Maven такие же, как упоминалось в наших предыдущихarticle. То есть Spring Core, Spring Batch и зависимость для базы данных (в нашем случаеSQLite).

4. конфигурация

В нашем вводномarticle мы видели пример преобразования некоторых финансовых данных из CSV в файл XML. Давайте продолжим тот же пример.

Здесь мы преобразуем финансовую информацию из 5 CSV-файлов в соответствующие XML-файлы, используя многопоточную реализацию.

Этого можно добиться с помощью одного разбиенияJob иStep. У нас будет пять потоков, по одной для каждого файла CSV.

Прежде всего, давайте создадим работу:

@Bean(name = "partitionerJob")
public Job partitionerJob()
  throws UnexpectedInputException, MalformedURLException, ParseException {
    return jobs.get("partitioningJob")
      .start(partitionStep())
      .build();
}

Как мы видим, этотJob начинается сPartitioningStep. Это наш главный шаг, который будет разделен на несколько этапов:

@Bean
public Step partitionStep()
  throws UnexpectedInputException, MalformedURLException, ParseException {
    return steps.get("partitionStep")
      .partitioner("slaveStep", partitioner())
      .step(slaveStep())
      .taskExecutor(taskExecutor())
      .build();
}

Здесь мы создадимPartitioningStep using the StepBuilderFactory. Для этого нам нужно предоставить информацию оSlaveSteps иPartitioner.

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

Давайте создадим его реализацию под названиемCustomMultiResourcePartitioner, где мы поместим имена входных и выходных файлов вExecutionContext, чтобы передать их на каждый подчиненный шаг:

public class CustomMultiResourcePartitioner implements Partitioner {

    @Override
    public Map partition(int gridSize) {
        Map map = new HashMap<>(gridSize);
        int i = 0, k = 1;
        for (Resource resource : resources) {
            ExecutionContext context = new ExecutionContext();
            Assert.state(resource.exists(), "Resource does not exist: "
              + resource);
            context.putString(keyName, resource.getFilename());
            context.putString("opFileName", "output"+k+++".xml");
            map.put(PARTITION_KEY + i, context);
            i++;
        }
        return map;
    }
}

Мы также создадим компонент для этого класса, где мы дадим исходный каталог для входных файлов:

@Bean
public CustomMultiResourcePartitioner partitioner() {
    CustomMultiResourcePartitioner partitioner
      = new CustomMultiResourcePartitioner();
    Resource[] resources;
    try {
        resources = resoursePatternResolver
          .getResources("file:src/main/resources/input/*.csv");
    } catch (IOException e) {
        throw new RuntimeException("I/O problems when resolving"
          + " the input file pattern.", e);
    }
    partitioner.setResources(resources);
    return partitioner;
}

Мы определим шаг раба, как и любой другой шаг с читателем и писателем. Читатель и писатель будут такими же, как мы видели в нашем вводном примере, за исключением того, что они получат параметр имени файла отStepExecutionContext.

Обратите внимание, что эти bean-компоненты должны иметь ступенчатую область видимости, чтобы они могли получать параметрыstepExecutionContext на каждом этапе. Если для них не будет ступенчатой ​​области видимости, их bean-компоненты будут созданы изначально и не будут принимать имена файлов на уровне шага:

@StepScope
@Bean
public FlatFileItemReader itemReader(
  @Value("#{stepExecutionContext[fileName]}") String filename)
  throws UnexpectedInputException, ParseException {

    FlatFileItemReader reader
      = new FlatFileItemReader<>();
    DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
    String[] tokens
      = {"username", "userid", "transactiondate", "amount"};
    tokenizer.setNames(tokens);
    reader.setResource(new ClassPathResource("input/" + filename));
    DefaultLineMapper lineMapper
      = new DefaultLineMapper<>();
    lineMapper.setLineTokenizer(tokenizer);
    lineMapper.setFieldSetMapper(new RecordFieldSetMapper());
    reader.setLinesToSkip(1);
    reader.setLineMapper(lineMapper);
    return reader;
}
@Bean
@StepScope
public ItemWriter itemWriter(Marshaller marshaller,
  @Value("#{stepExecutionContext[opFileName]}") String filename)
  throws MalformedURLException {
    StaxEventItemWriter itemWriter
      = new StaxEventItemWriter();
    itemWriter.setMarshaller(marshaller);
    itemWriter.setRootTagName("transactionRecord");
    itemWriter.setResource(new ClassPathResource("xml/" + filename));
    return itemWriter;
}

Упоминая читателя и писателя на этапе ведомого устройства, мы можем передать аргументы как null, потому что эти имена файлов не будут использоваться, поскольку они получат имена файлов изstepExecutionContext:

@Bean
public Step slaveStep()
  throws UnexpectedInputException, MalformedURLException, ParseException {
    return steps.get("slaveStep").chunk(1)
      .reader(itemReader(null))
      .writer(itemWriter(marshaller(), null))
      .build();
}

5. Заключение

В этом руководстве мы обсудили, как реализовать работу с параллельной обработкой с использованием Spring Batch.

Как всегда, доступна полная реализация этого примераover on GitHub.