Spring Batch mit Partitioner

Spring Batch mit Partitioner

1. Überblick

In unseren vorherigenintroduction to Spring Batch haben wir das Framework als Stapelverarbeitungswerkzeug eingeführt. Wir haben auch die Konfigurationsdetails und die Implementierung für eine Single-Threaded-Jobausführung mit nur einem Prozess untersucht.

Um einen Job mit einer gewissen Parallelverarbeitung zu implementieren, wird eine Reihe von Optionen bereitgestellt. Auf einer höheren Ebene gibt es zwei Arten der Parallelverarbeitung:

  1. Single-Process, Multi-Threaded

  2. Multi-Prozess

In diesem kurzen Artikel wird die Partitionierung vonStep erläutert, die sowohl für Einzelprozess- als auch für Mehrprozessjobs implementiert werden kann.

2. Einen Schritt partitionieren

Spring Batch mit Partitionierung bietet uns die Möglichkeit, die Ausführung vonStep zu teilen:

image

Das obige Bild zeigt eine Implementierung von aJob mit einem partitioniertenStep.

Es gibt einStepnamens "Master", dessen Ausführung in einige "Slave" -Schritte unterteilt ist. Diese Slaves können den Platz eines Masters einnehmen, und das Ergebnis bleibt unverändert. Sowohl Master als auch Slave sind Instanzen vonStep. Slaves können Remotedienste sein oder Threads nur lokal ausführen.

Bei Bedarf können wir Daten vom Master an den Slave übergeben. Die Metadaten (d. H. JobRepository) stellt sicher, dass jeder Slave nur einmal in einer einzelnen Ausführung vonJob. ausgeführt wird

Das folgende Sequenzdiagramm zeigt, wie alles funktioniert:

image

Wie gezeigt, treibtPartitionStep die Ausführung an. DasPartitionHandler ist dafür verantwortlich, die Arbeit von „Master“ in die „Slaves“ aufzuteilen. Das am weitesten rechts stehendeStep ist der Slave.

3. Der Maven POM

Die Maven-Abhängigkeiten sind die gleichen wie in unseren vorherigenarticle. Das heißt, Spring Core, Spring Batch und die Abhängigkeit für die Datenbank (in unserem FallSQLite).

4. Aufbau

In unserer Einführungarticle haben wir ein Beispiel für die Konvertierung einiger Finanzdaten von CSV in XML-Dateien gesehen. Lassen Sie uns das gleiche Beispiel erweitern.

Hier werden die Finanzinformationen von 5 CSV-Dateien in entsprechende XML-Dateien konvertiert, wobei eine Multithread-Implementierung verwendet wird.

Wir können dies mit einer einzelnen Partitionierung vonJob undSteperreichen. Wir haben fünf Threads, einen für jede der CSV-Dateien.

Zunächst erstellen wir einen Job:

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

Wie wir sehen können, beginnt diesesJob mitPartitioningStep. Dies ist unser Master-Schritt, der in verschiedene Slave-Schritte unterteilt wird:

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

Hier erstellen wir diePartitioningStep using the StepBuilderFactory. Dazu müssen wir die Informationen überSlaveSteps undPartitioner angeben.

DasPartitioner ist eine Schnittstelle, die die Möglichkeit bietet, einen Satz von Eingabewerten für jeden der Slaves zu definieren. Mit anderen Worten, Logik, um Aufgaben in entsprechende Threads zu unterteilen, wird hier verwendet.

Erstellen wir eine Implementierung mit dem NamenCustomMultiResourcePartitioner, in der die Namen der Eingabe- und Ausgabedateien inExecutionContext eingefügt werden, um sie an jeden Slave-Schritt weiterzuleiten:

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

Wir erstellen auch das Bean für diese Klasse, in dem wir das Quellverzeichnis für Eingabedateien angeben:

@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;
}

Wir werden den Slave-Schritt definieren, genau wie jeden anderen Schritt mit dem Leser und dem Schreiber. Der Leser und der Schreiber sind dieselben wie in unserem Einführungsbeispiel, außer dass sie den Dateinamenparameter vonStepExecutionContext. erhalten

Beachten Sie, dass diese Beans schrittweise festgelegt werden müssen, damit sie bei jedem Schritt die ParameterstepExecutionContextempfangen können. Wenn sie keinen Schrittbereich haben, werden ihre Beans zunächst erstellt und akzeptieren die Dateinamen auf Stufenebene nicht:

@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;
}

Während wir den Leser und Schreiber im Slave-Schritt erwähnen, können wir die Argumente als null übergeben, da diese Dateinamen nicht verwendet werden, da sie die Dateinamen vonstepExecutionContext erhalten:

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

5. Fazit

In diesem Lernprogramm wurde die Implementierung eines Jobs mit paralleler Verarbeitung mithilfe von Spring Batch erläutert.

Wie immer ist die vollständige Implementierung für dieses Beispielover on GitHub verfügbar.