實戰程式碼(二):Springboot Batch實現定時資料遷移
一、理論基礎
1.1 Batch是什麼
Spring Batch是Spring全家桶中的一員,是一個輕量級的批處理框架,比較實際的應用場景是資料遷移,比如將csv檔案中的資料遷移到MySQL。
優勢在於上手簡單,編碼規範化,能以較少的程式碼實現強大的功能。和ETL工具-kettle功能類似,但是定製性比較強
應用場景集中在各種DB、檔案等各種已經存在的歷史資料,貌似不支援訊息佇列的實時監聽(如果有知道如何實現的,一定要告訴我),實時資料監聽可以使用Storm等流式資料處理框架
1.2 基礎概念
- ItemReader:讀取資料,有多個封裝好的類,可以支援多種資料來源,如csv、jdbc等,也可以自定義功能實現。
- ItemWriter:輸出資料,有Reader配套的封裝類,同樣可以自定義功能實現,如輸出到訊息佇列。
- ItemProcessor:資料處理模組,輸入為Reader讀取的資料,輸出為Writer的輸入。
- Step:資料操作的步驟,包括:ItemReader->ItemProcessor->ItemWriter 整個資料流
- Job:待執行的任務,每個job可以有一個或多個step
- JobRepository:註冊job的容器
- JobLauncher:啟動job
- JobLocator:可以根據jobName獲取到指定的job,可以配合JobRepository、JobLauncher來手動啟動job
1.3 如何開發一個Batch並啟動
- 確認輸入輸出,分別定義InputEntity和OutputEntity
- 編寫Reader,輸入為各種資料來源(csv、MySQL等),輸出為InputEntity,資料庫的可以選擇封裝好的類: JdbcCursorItemReader
- 編寫Processor,輸入為InputEntity,輸出為OutputEntity,繼承ItemProcessor<T, T>,實現process方法即可
- 編寫Writer,輸入為OutputEntity,輸出為指定的資料來源(MySQL等)
- 配置Step和Job
拋卻必要配置,實現一個遷移任務就是這麼簡單
二、實戰程式碼
2.0 建立測試表
資料來源表
CREATE TABLE `article` (
`title` varchar(64) DEFAULT NULL COMMENT '標題',
`content` varchar(255) DEFAULT NULL COMMENT '內容',
`event_occurred_time` varchar(32) DEFAULT NULL COMMENT '事件發生時間'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章';
輸出的資料表
CREATE TABLE `article_detail` (
`title` varchar(64) DEFAULT NULL COMMENT '標題',
`content` varchar(255) DEFAULT NULL COMMENT '內容',
`event_occurred_time` varchar(32) DEFAULT NULL COMMENT '事件發生時間',
`source` varchar(255) DEFAULT NULL COMMENT '文章來源',
`description` varchar(255) DEFAULT NULL COMMENT '描述資訊'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章詳情';
2.1 依賴引入
# 本例項基於Springboot 2.X版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
2.2 配置檔案
spring:
batch:
job:
# 預設為true,程式啟動時Job會自動執行;false,需要手動啟動任務(jobLaucher.run)
enabled: false
# spring batch預設情況下需要在資料庫中建立後設資料表,always:每次都會檢查表存不存在,不存在會自動建立;never:不會自動建立,如果表不存在,則會報錯;
initialize-schema: never
如需手動建立後設資料表,請參考最後面的附錄
2.3 配置JobRepository
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry){
JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry);
return jobRegistryBeanPostProcessor;
}
如果沒有該項配置,則手動啟動時會報錯No job configuration with the name [XJob] was registered
2.4 可選配置
2.4.1 記憶體模式
/**
* - NoPersistence 無持久化
*/
@Component
public class NoPersistenceBatchConfigurer extends DefaultBatchConfigurer {
@Override
public void setDataSource(DataSource dataSource) {
}
}
加了此項配置後,不會在資料庫中建立後設資料表,所有的job都是在記憶體中管理。程式重啟後,任務資訊會丟失,複雜的任務場景不建議加此配置,對於不需要嚴格任務管理的任務來講比較合適。
2.4.2 任務監聽
@Component
@Slf4j
public class JobListener extends JobExecutionListenerSupport {
@Override
public void afterJob(JobExecution jobExecution) {
if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
log.info("任務[{}]執行成功,引數:[{}]", jobExecution.getJobInstance().getJobName(),
jobExecution.getJobParameters().getString("executedTime"));
} else {
log.info("任務[{}]執行失敗", jobExecution.getJobInstance().getJobName());
// TODO something
}
}
}
如果不需要在任務成功或者失敗後做一些操作的話可以不加監聽器,因為Batch自身包含日誌執行情況日誌(info級別),包括執行結果、執行引數、執行耗費時間等
2.5 定義輸入、輸出實體
Article:輸入
@Data
public class Article {
private String title;
private String content;
private String eventOccurredTime;
}
ArticleDetail:待輸出的資料結構
@Data
public class ArticleDetail {
private String title;
private String content;
private String eventOccurredTime;
private String source;
private String description;
}
2.6 Reader
2.6.1 JdbcCursorItemReader
/**
* 普通讀取模式
* - MySQL會將所有的紀錄讀到記憶體中
* - 資料量大的話記憶體佔用會很高
*/
public JdbcCursorItemReader<Article> getArticle(String executedTime) {
String lastExecutedTime = "2020-01-01 00:00:00";
String sql = StringUtils.join("SELECT * FROM article WHERE event_occurred_time >= '",
lastExecutedTime, "' AND event_occurred_time < '", executedTime, "'");
return new JdbcCursorItemReaderBuilder<Article>()
.dataSource(dataSource)
.sql(sql)
.fetchSize(10)
.name("getArticle")
.beanRowMapper(Article.class)
.build();
}
2.6.2 分頁讀取
/**
* 分頁讀取模式
* - 只要分頁合理配置,記憶體佔用可控
*/
public JdbcPagingItemReader<Article> getArticlePaging(String executedTime) {
String lastExecutedTime = "";
Map<String, Object> parameterValues = new HashMap<>(2);
parameterValues.put("startTime", lastExecutedTime);
parameterValues.put("stopTime", executedTime);
return new JdbcPagingItemReaderBuilder<Article>()
.dataSource(dataSource)
.name("getArticlePaging")
.fetchSize(10)
.parameterValues(parameterValues)
.pageSize(10)
.rowMapper(new ArticleMapper())
.queryProvider(articleProvider())
.build();
}
private PagingQueryProvider articleProvider() {
Map<String, Order> sortKeys = new HashMap<>(1);
sortKeys.put("event_occurred_time", Order.ASCENDING);
MySqlPagingQueryProvider provider = new MySqlPagingQueryProvider();
provider.setSelectClause("title, content, event_occurred_time");
provider.setFromClause("article");
provider.setWhereClause("event_occurred_time >= :startTime AND event_occurred_time < :stopTime");
provider.setSortKeys(sortKeys);
return provider;
}
2.6.3 說明
- 可以繼承ItemReader,實現自定義功能的Reader
- 分頁雖然對於資源的使用時可控的,但是效率會低很多,需要合理設定每一頁的資料量。
- 如果有很多個任務一起執行,是看總資料量,比如有五個任務,每個任務採集的資料量為10W,那麼設定分頁的時候,要考慮到50W的資料量的記憶體佔用情況
- JdbcCursorItemReader在記憶體足夠的情況下可以使用,效率很高
2.7 Processor
2.7.1 示例程式碼
@Component
public class ArticleProcessor implements ItemProcessor<Article, ArticleDetail> {
@Override
public ArticleDetail process(Article data) throws Exception {
ArticleDetail articleDetail = new ArticleDetail();
BeanUtils.copyProperties(data, articleDetail);
articleDetail.setSource("weibo");
articleDetail.setDescription("這是一條來源於微博的新聞");
return articleDetail;
}
}
2.7.2 說明
- processor只需要繼承ItemProcessor<T1, T2>實現其中的process方法即可。
- T1是Reader讀取的資料實體
- T2是要輸出到Writer的資料實體,也就是Writer的輸入資料實體
2.8 Writer
2.8.1 JdbcBatchItemWriter
@Component
public class ArticleJdbcWriter {
private final DataSource dataSource;
public ArticleJdbcWriter(DataSource dataSource) {
this.dataSource = dataSource;
}
public JdbcBatchItemWriter<ArticleDetail> writer() {
return new JdbcBatchItemWriterBuilder<ArticleDetail>()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO article_detail (title, content, event_occurred_time, source, description) VALUES (:title, :content, :eventOccurredTime, :source, :description)")
.dataSource(dataSource)
.build();
}
}
2.8.2 自定義writer
@Slf4j
public class ArticleWriter implements ItemWriter<ArticleDetail> {
@Override
public void write(List<? extends ArticleDetail> list) throws Exception {
log.info("list的大小等於job中設定的chunkSize, size = {}", list.size());
// TODO 此處可輸出資料,比如輸出到訊息佇列
list.forEach(article -> log.info("輸出測試,title:{}", article.getTitle()));
}
}
2.8.3 說明
- 繼承ItemWriter,實現writer方法即可
- T是Processor的輸出
- list是Step中設定的chunkSize,也就是每次提交到writer的資料量
2.9 Step與Job
2.9.1 示例程式碼
@Configuration
@EnableBatchProcessing
public class ArticleBatchJob {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Autowired
private ArticleReaderDemo articleReader;
@Autowired
private ArticleProcessor articleProcessor;
@Autowired
private ArticleJdbcWriter articleJdbcWriter;
@Bean(name = "articleReader")
@StepScope
public JdbcPagingItemReader<Article> batchReader(@Value("#{jobParameters['executedTime']}") String executedTime) {
return articleReader.getArticlePaging(executedTime);
}
@Bean(name = "articleWriter")
public ItemWriter<ArticleDetail> batchWriter() {
// return articleJdbcWriter.writer();
return new ArticleWriter();
}
@Bean(name = "articleJob")
public Job batchJob(JobListener listener, Step articleStep) {
return jobBuilderFactory.get("articleJob")
.listener(listener)
.incrementer(new RunIdIncrementer())
.flow(articleStep)
.end()
.build();
}
@Bean(name = "articleStep")
public Step step(JdbcPagingItemReader<Article> articleReader, ItemWriter<ArticleDetail> articleWriter) {
return stepBuilderFactory.get("crossHistoryStep")
// 資料會累積到一定量再提交到writer
.<Article, ArticleDetail>chunk(10)
.reader(articleReader)
.processor(articleProcessor)
.writer(articleWriter)
// 預設為false(如果引數未發生變化的話,任務不會重複執行)
.allowStartIfComplete(true)
.build();
}
}
2.9.1 說明
- @EnableBatchProcessing是必須的
- 每個Step中,並不是每處理一條資料都提交到Writer的,需要配置chunkSize,合理的chunkSize對於資料採集效率的提升效果很明顯
- Job如果執行成功一次,下次任務啟動時如果引數沒有變化的話,預設情況下是不會重複執行的,如果想要執行可以傳一個時間引數或者設定
allowStartIfComplete(true)
2.10 整合Quartz實現定時啟動
Springboot如何整合Quartz可以看 《實戰程式碼(一):SpringBoot整合Quartz》
2.10.1 QuartzJob
@Component
@Slf4j
@DisallowConcurrentExecution
public class ArticleQuartzJob extends QuartzJobBean {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private JobLocator jobLocator;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
Job job = jobLocator.getJob("articleJob");
jobLauncher.run(job, new JobParametersBuilder()
.addString("executedTime", "2020-11-10 16:21:01")
.toJobParameters());
} catch (Exception e) {
e.printStackTrace();
log.error("任務[articleJob]啟動失敗,錯誤資訊:{}", e.getMessage());
}
}
}
2.10.2 初始化QuartzJob
@Component
public class QuartzJobInit implements CommandLineRunner {
@Autowired
private QuartzUtils quartzUtils;
@Override
public void run(String... args) throws Exception {
quartzUtils.addSingleJob(ArticleQuartzJob.class, "articleJob", 60);
}
}
原始碼地址
https://github.com/lysmile/spring-boot-demo/tree/master/spring-boot-batch-demo
附錄 後設資料表建表語句(MYSQL)
建立後設資料表的SQL檔案在
org.springframework.batch.core
包中可以找到,可以針對不同的資料庫進行配置
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB_INSTANCE (
JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT ,
JOB_NAME VARCHAR(100) NOT NULL,
JOB_KEY VARCHAR(32) NOT NULL,
constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ENGINE=InnoDB;
CREATE TABLE BATCH_JOB_EXECUTION (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT ,
JOB_INSTANCE_ID BIGINT NOT NULL,
CREATE_TIME DATETIME NOT NULL,
START_TIME DATETIME DEFAULT NULL ,
END_TIME DATETIME DEFAULT NULL ,
STATUS VARCHAR(10) ,
EXIT_CODE VARCHAR(2500) ,
EXIT_MESSAGE VARCHAR(2500) ,
LAST_UPDATED DATETIME,
JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_JOB_EXECUTION_PARAMS (
JOB_EXECUTION_ID BIGINT NOT NULL ,
TYPE_CD VARCHAR(6) NOT NULL ,
KEY_NAME VARCHAR(100) NOT NULL ,
STRING_VAL VARCHAR(250) ,
DATE_VAL DATETIME DEFAULT NULL ,
LONG_VAL BIGINT ,
DOUBLE_VAL DOUBLE PRECISION ,
IDENTIFYING CHAR(1) NOT NULL ,
constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_STEP_EXECUTION (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME DATETIME NOT NULL ,
END_TIME DATETIME DEFAULT NULL ,
STATUS VARCHAR(10) ,
COMMIT_COUNT BIGINT ,
READ_COUNT BIGINT ,
FILTER_COUNT BIGINT ,
WRITE_COUNT BIGINT ,
READ_SKIP_COUNT BIGINT ,
WRITE_SKIP_COUNT BIGINT ,
PROCESS_SKIP_COUNT BIGINT ,
ROLLBACK_COUNT BIGINT ,
EXIT_CODE VARCHAR(2500) ,
EXIT_MESSAGE VARCHAR(2500) ,
LAST_UPDATED DATETIME,
constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT TEXT ,
constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT TEXT ,
constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;
CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
ID BIGINT NOT NULL,
UNIQUE_KEY CHAR(1) NOT NULL,
constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;
INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_STEP_EXECUTION_SEQ);
CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
ID BIGINT NOT NULL,
UNIQUE_KEY CHAR(1) NOT NULL,
constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;
INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_EXECUTION_SEQ);
CREATE TABLE BATCH_JOB_SEQ (
ID BIGINT NOT NULL,
UNIQUE_KEY CHAR(1) NOT NULL,
constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;
INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_SEQ);
參考
相關文章
- 【Redis 技術探索】「資料遷移實戰」手把手教你如何實現線上 + 離線模式進行遷移 Redis 資料實戰指南(scan模式遷移)Redis模式
- Mysql百萬級資料遷移,怎麼遷移?實戰過沒?MySql
- 【Redis 技術探索】「資料遷移實戰」手把手教你如何實現線上 + 離線模式進行遷移Redis資料實戰指南(離線同步資料)Redis模式
- Mysql百萬級資料遷移實戰筆記MySql筆記
- kafka資料遷移實踐Kafka
- Mongo資料遷移實驗Go
- 有贊大資料離線叢集遷移實戰大資料
- dataguard備庫的資料檔案的遷移實戰
- 二維網格的遷移(java實現)Java
- 快速實現地圖遷移資料視覺化地圖視覺化
- SpringBoot實戰:輕鬆實現介面資料脫敏Spring Boot
- 十餘行程式碼完成遷移學習,PaddleHub實戰篇行程遷移學習
- 利用PLSQL實現表空間的遷移(二)SQL
- 資料庫遷移之資料泵實驗資料庫
- 資料遷移方案 + Elasticsearch在綜合搜尋列表實現Elasticsearch
- 利用MongoDB的SplitVector命令實現併發資料遷移MongoDB
- SpringBoot如何實現定時任務Spring Boot
- ORM實操之資料庫遷移ORM資料庫
- 快速實現本地資料備份與FTP遠端資料遷移FTP
- 實現彩色二維碼程式碼實
- 一次用rman做資料遷移的實戰經歷
- 資料遷移指令碼指令碼
- 多程式PHP指令碼實現海量資料轉移總結PHP指令碼
- 【SpringBoot實戰】資料訪問Spring Boot
- RMAN COPY實現ORACLE資料庫儲存遷移的方案Oracle資料庫
- 實戰程式碼(一):SpringBoot整合QuartzSpring Bootquartz
- 同版本的庚頓實時資料庫的資料遷移操作步驟資料庫
- java springboot 實現定時器任務JavaSpring Boot定時器
- cassandra百億級資料庫遷移實踐資料庫
- Jenkins搭建與資料遷移實踐Jenkins
- Datapump資料遷移的實踐總結
- 遷移 SQL Server 到 Azure SQL 實戰SQLServer
- 線上資料遷移,數字化時代的必修課 —— 京東雲資料遷移實踐
- 實戰案例丨使用雲連線CC和資料複製服務DRS實現跨區域RDS遷移和資料同步
- 使用SQL SERVER儲存過程實現歷史資料遷移SQLServer儲存過程
- 【資料遷移】RMAN遷移資料庫到ASM(二)切換資料檔案到ASM資料庫ASM
- Hadoop資料遷移MaxCompute最佳實踐Hadoop
- 資料庫平滑遷移方案與實踐分享資料庫