自計算機使用興起以來,公司出於不同的目的始終依賴批處理資料,要麼是在應用程式之間移動資料 (ETL),要麼是進行一些需要很長時間才能實時完成的平行計算。
處理大量資料的挑戰始終在於如何充分利用可用的計算資源,從而最佳化時間和成本。
在批處理這個領域,許多解決方案都將自己作為標準,其中之一就是Spring Batch,它是 JAVA 世界中構建高效能和最佳化的批處理應用程式的事實上的標準。
案例準備
這裡將透過一個簡單的作業來演示此功能,該作業執行從資料庫讀取資料、對其應用一些轉換,最後將其寫入檔案。
資料表:
create table transactions ( id integer not null, transaction_date date not null, amount numeric not null, created_at date, constraint pk_transactions primary key(id) );
|
為了初始化資料庫,我建立了以下 postgres 指令碼,以向事務表中填充 1 億條記錄。
DO $$ <<block>> declare counter integer := 0; rec RECORD;
begin -- for rec in ( with nums as ( SELECT a id FROM generate_series(1, 1000000) as s(a) ), dates as( select row_number() OVER (ORDER BY a) line, a::date as date from generate_series( '2020-01-01'::date, '2020-12-31'::date, '1 day' ) s(a) ) select row_number() OVER (ORDER BY id) id, d.date, 200*random() amount from nums n, dates d where d.line <= 100 ) loop insert into transactions values (rec.id, rec.date, rec.amount, CURRENT_TIMESTAMP);
counter := counter + 1;
if MOD(counter, 10000) = 0 then raise notice 'Commiting at : %', counter; commit ; end if; end loop;
raise notice 'Value: %', counter;
END block $$;
|
為了模擬 1 毫秒的處理時間,我新增了對 Thread.sleep 方法的呼叫。在現實生活中,處理邏輯可能非常複雜,每個專案的處理時間可能不止 1 毫秒(例如:呼叫外部網路服務)。
@Bean public ItemProcessor<TransactionVO, TransactionVO>. multithreadedchProcessor() { return (transaction) -> { Thread.sleep(1); return transaction; }; }
|
在單執行緒處理過程中,讀取、處理和寫入都是在單執行緒中同步執行的。
多執行緒步驟
在本文中,我們瞭解瞭如何使用 TaskExecutor 對作業步驟進行多執行緒處理,從而與單執行緒步驟相比獲得大量處理時間。
多執行緒在步驟級別啟用,並且它在自己的執行緒中執行每個塊。
在 Spring Batch 作業中使用多個執行緒非常簡單。您定義一個taskExecutor並在相關步驟中引用它。這會為每個資料塊建立一個新執行緒,同時處理它們。這可以顯著提高效能。
然而,大多數 Spring Batch 讀者都持有狀態(進度資訊)。這對於作業重新啟動很有用。在多執行緒環境中,如果不正確同步,此類有狀態讀取器可能會導致資料不一致。
@Bean public Step multithreadedManagerStep(StepBuilderFactory stepBuilderFactory) throws Exception { return stepBuilderFactory .get(<font>"Multithreaded : Read -> Process -> Write ") .<TransactionVO, TransactionVO>chunk(1000) .reader(multithreadedcReader(null)) .processor(multithreadedchProcessor()) .writer(multithreadedcWriter()) .taskExecutor(taskExecutor()) .build(); } @Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(64); executor.setMaxPoolSize(64); executor.setQueueCapacity(64); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setThreadNamePrefix("MultiThreaded-"); return executor; }
|
以下是整個多執行緒演示程式的原始碼。
@SpringBootApplication @EnableBatchProcessing public class MultithreadedBatchPerformanceApplication {
public static void main(String[] args) { SpringApplication.run(MultithreadedBatchPerformanceApplication.class, args); }
@Bean public Job multithreadedJob(JobBuilderFactory jobBuilderFactory) throws Exception { return jobBuilderFactory .get(<font>"Multithreaded JOB") .incrementer(new RunIdIncrementer()) .flow(multithreadedManagerStep(null)) .end() .build(); }
@Bean public Step multithreadedManagerStep(StepBuilderFactory stepBuilderFactory) throws Exception { return stepBuilderFactory .get("Multithreaded : Read -> Process -> Write ") .<TransactionVO, TransactionVO>chunk(1000) .reader(multithreadedcReader(null)) .processor(multithreadedchProcessor()) .writer(multithreadedcWriter()) .taskExecutor(taskExecutor()) .build(); }
@Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(64); executor.setMaxPoolSize(64); executor.setQueueCapacity(64); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setThreadNamePrefix("MultiThreaded-"); return executor; }
@Bean public ItemProcessor<TransactionVO, TransactionVO> multithreadedchProcessor() { return (transaction) -> { Thread.sleep(1); //og.info(Thread.currentThread().getName());<i> return transaction; }; }
@Bean public ItemReader<TransactionVO> multithreadedcReader(DataSource dataSource) throws Exception {
return new JdbcPagingItemReaderBuilder<TransactionVO>() .name("Reader") .dataSource(dataSource) .selectClause("SELECT * ") .fromClause("FROM transactions ") .whereClause("WHERE ID <= 1000000 ") .sortKeys(Collections.singletonMap("ID", Order.ASCENDING)) .rowMapper(new TransactionVORowMapper()) .build(); }
@Bean public FlatFileItemWriter<TransactionVO> multithreadedcWriter() {
return new FlatFileItemWriterBuilder<TransactionVO>() .name("Writer") .append(false) .resource(new FileSystemResource("transactions.txt")) .lineAggregator(new DelimitedLineAggregator<TransactionVO>() { { setDelimiter(";"); setFieldExtractor(new BeanWrapperFieldExtractor<TransactionVO>() { { setNames(new String[]{"id", "date", "amount", "createdAt"}); } }); } }) .build(); } }
|
執行該批處理後,大約 6 分鐘就能將 100 萬條記錄寫入檔案。
任務:[FlowJob: [name=Synchronous JOB]] 已完成,引數如下:{run.id=10}]和以下狀態:[完成],時間為 6m12s265ms
在這種規模下,單執行緒步驟處理 100 萬個專案所需的時間幾乎是多執行緒步驟的 4 倍。考慮到在我們的示例中,處理時間為 1ms/條目,這是一個巨大的收益。
缺點
由於配置簡單,多執行緒步驟比其他功能具有巨大的優勢,但它們也不乏缺點,其中:
- 作業無法重新啟動。如果作業失敗,它就無法從原來的位置繼續執行
- 沒有處理物品的訂單保證
- 您應該考慮執行緒安全(示例可能不使用基於遊標的 JDBC 專案讀取器)
非同步處理
非同步處理是另一種提升 Spring Batch 效能的技術。它將透過在單獨的執行緒中執行來擴充套件每個專案的處理,一旦完成,它就會返回AsyncItemWriter 將處理的Future 。
當步驟的瓶頸是處理時,這種縮放技術特別有用,例如,您有一個讀取器從 csv 檔案讀取專案,並且對於每個專案讀取,專案處理器需要訪問外部 API 並執行一些複雜的操作計算,然後將結果寫入目的地。
在 Spring 批處理中有兩個類可以幫助實現此機制:AsyncItemProcessor 和 AsyncItemWriter,它們分別是 ItemProcess 和 ItemWriter 的裝飾器。 AsyncItemWriter 只是為了方便解開不同 AsyncItemProcessor 返回的Futures 。
過使用 2 個裝飾器類啟用非同步處理:
該類將處理委託給 ItemProcessor,並允許透過設定taskExecutor 進行多執行緒處理。@Bean public AsyncItemProcessor<TransactionVO, TransactionVO> asyncProcessor() { AsyncItemProcessor<TransactionVO, TransactionVO> asyncItemProcessor = new AsyncItemProcessor<>(); asyncItemProcessor.setDelegate(itemProcessor()); asyncItemProcessor.setTaskExecutor(taskExecutor());
return asyncItemProcessor; } @Bean public ItemProcessor<TransactionVO, TransactionVO> itemProcessor() { return (transaction) -> { Thread.sleep(1); return transaction; }; }
|
由專案處理器處理的專案被包裝到Future中並傳遞給編寫器以解開包裝並寫入其目的地。@Bean public AsyncItemWriter<TransactionVO> asyncWriter() { AsyncItemWriter<TransactionVO> asyncItemWriter = new AsyncItemWriter<>(); asyncItemWriter.setDelegate(itemWriter()); return asyncItemWriter; }
|
以下是整個非同步處理演示程式的原始碼。
@SpringBootApplication @EnableBatchProcessing public class AsyncProcessingBatchPerformanceApplication {
public static void main(String[] args) { SpringApplication.run(AsyncProcessingBatchPerformanceApplication.class, args); }
@Bean public Job asyncJob(JobBuilderFactory jobBuilderFactory) { return jobBuilderFactory .get(<font>"Asynchronous Processing JOB") .incrementer(new RunIdIncrementer()) .flow(asyncManagerStep(null)) .end() .build(); }
@Bean public Step asyncManagerStep(StepBuilderFactory stepBuilderFactory) { return stepBuilderFactory .get("Asynchronous Processing : Read -> Process -> Write ") .<TransactionVO, Future<TransactionVO>>chunk(1000) .reader(asyncReader(null)) .processor(asyncProcessor()) .writer(asyncWriter()) .taskExecutor(taskExecutor()) .build(); }
@Bean public AsyncItemProcessor<TransactionVO, TransactionVO> asyncProcessor() { AsyncItemProcessor<TransactionVO, TransactionVO> asyncItemProcessor = new AsyncItemProcessor<>(); asyncItemProcessor.setDelegate(itemProcessor()); asyncItemProcessor.setTaskExecutor(taskExecutor());
return asyncItemProcessor; }
@Bean public AsyncItemWriter<TransactionVO> asyncWriter() { AsyncItemWriter<TransactionVO> asyncItemWriter = new AsyncItemWriter<>(); asyncItemWriter.setDelegate(itemWriter()); return asyncItemWriter; }
@Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(64); executor.setMaxPoolSize(64); executor.setQueueCapacity(64); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setThreadNamePrefix("MultiThreaded-"); return executor; }
@Bean public ItemProcessor<TransactionVO, TransactionVO> itemProcessor() { return (transaction) -> { Thread.sleep(1); return transaction; }; }
@Bean public ItemReader<TransactionVO> asyncReader(DataSource dataSource) {
return new JdbcPagingItemReaderBuilder<TransactionVO>() .name("Reader") .dataSource(dataSource) .selectClause("SELECT * ") .fromClause("FROM transactions ") .whereClause("WHERE ID <= 1000000 ") .sortKeys(Collections.singletonMap("ID", Order.ASCENDING)) .rowMapper(new TransactionVORowMapper()) .build(); }
@Bean public FlatFileItemWriter<TransactionVO> itemWriter() {
return new FlatFileItemWriterBuilder<TransactionVO>() .name("Writer") .append(false) .resource(new FileSystemResource("transactions.txt")) .lineAggregator(new DelimitedLineAggregator<TransactionVO>() { { setDelimiter(";"); setFieldExtractor(new BeanWrapperFieldExtractor<TransactionVO>() { { setNames(new String[]{"id", "date", "amount", "createdAt"}); } }); } }) .build(); } }
|
執行該批處理後,大約 2 分鐘就能將 100 萬條記錄寫入檔案。任務:[FlowJob: [name=Asynchronous Processing JOB]] 已完成,引數如下:{run.id=2}]和以下狀態:[完成],時間為 2m6s76ms
缺點
本文的原始碼可以在GitHub 儲存庫中找到。