Spring Batch批量處理支付寶賬單實踐-進階篇

前言

之前一篇寫了SpringBatch批量加載支付寶賬單的基礎篇http://www.lxweimin.com/p/6f038c1f6037,實現了將支付寶賬單通過springbatch加載、邏輯加工后、輸出到自己定義的賬單格式文件,上篇也說了只是介紹了基本使用,本篇是上一篇的進階,還是會繼續基于Springbatch全程使用javaconfig的方式實現,數據加載入庫、異常數據處理、并行、定時任務等,在寫這篇文章前,發現全網寫的關于Springbatch的文章絕大部分都是基于XML配置的,隨著springboot的逐漸普及,大家也都習慣了拋棄xml,使用javaconfig來配置項目,但是網上的blog包括spring官網對springbatch的javaconfig都介紹的很少,本篇就幫大家通過javaconfig配置整個springbatch,并實現一些高級用法。

1. 加載數據到數據庫

在批量過程中一般都需要將數據持久化,所以我們介紹下如何將批量數據加載到傳統的RDBMS數據庫中,我這里使用的是mysql,對于使用oracle或者其他數據庫的同學,直接替換datasource就可以了。

首先在pom中加上jdbc和mysql-connect依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
        </dependency>

在配置文件中加上數據庫的配置

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=XXXX
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

加上Datasource Config配置類

@Configuration
public class MySQLDataSourceConfig {
    @Bean(name = "mysqlDataSource")
    @Qualifier("mysqlDataSource")
    @ConfigurationProperties(prefix="spring.datasource")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "mysqldbcTemplate")
    public JdbcTemplate mysqlJdbcTemplate(
            @Qualifier("mysqlDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

這里實現一個數據庫寫入的功能,就是將從支付寶賬單中讀取的數據寫入數據庫的ItemWriter類,之前有讀者說賬單格式不對,這里解釋下這里用的賬單是我從網上隨便搜的支付寶賬單和實際支付寶商戶使用的賬單格式是有比較大的差異,這里只是做個例子,讓大家學習下springbatch,實際使用過程中還是要改下數據結構以官方提供的加載數據結構為準。新建AlipayDBItemWriter類,這個類實現了ItemWriter接口,上一篇文章也介紹了實現該接口需要實現write(list)方法,整體的邏輯比較簡單就是將從ItemReader中讀取到的數據加載到數據庫中,代碼一看就懂。

@Service
public class AlipayDBItemWriter implements ItemWriter<AlipayTranDO> {
    private static final String INSERT_ALYPAY_TRAN =
            "insert into alipay_tran_today(tran_id, channel, tran_type, counter_party, goods, amount, is_debit_credit, state) values(?,?,?,?,?,?,?,?)";

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void write(List<? extends AlipayTranDO> list) throws Exception {
        for(AlipayTranDO alipayTran : list){
            jdbcTemplate.update(INSERT_ALYPAY_TRAN,
                    alipayTran.getTranId(),
                    alipayTran.getChannel(),
                    alipayTran.getTranType(),
                    alipayTran.getCounterparty(),
                    alipayTran.getGoods(),
                    alipayTran.getAmount(),
                    alipayTran.getIsDebitCredit(),
                    alipayTran.getState());
        }
    }
}

寫好了ItemWriter最后就是寫batchconfig了,直接將上一篇的step1()方法復制一份改為step2,將writer改為剛才新建的AlipayDBItemWriter,將step2()放到job中,直接運行就可以去數據庫看結果了,這里要注意下要提前到數據庫中建好表。

@Bean
    public Step step2() {
        return stepBuilderFactory.get("step2")
                .<AlipayTranDO, AlipayTranDO> chunk(10)
                .reader(alipayFileItemReader.getMultiAliReader())
                .writer(alipayDBItemWriter)
                .build();
    }

alipay_tran_today表結構

CREATE TABLE `alipay_tran_today` (
  `tran_id` varchar(40) DEFAULT NULL,
  `channel` varchar(20) DEFAULT NULL,
  `tran_type` varchar(10) DEFAULT NULL,
  `counter_party` varchar(20) DEFAULT NULL,
  `goods` varchar(40) DEFAULT NULL,
  `amount` varchar(20) DEFAULT NULL,
  `is_debit_credit` varchar(10) DEFAULT NULL,
  `state` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2. 批量數據校驗清洗

下面我們再介紹下批量數據的校驗清洗,如果通過springbatch來實現,在實現批量的過程中,我們在加載數據前都會去判斷加載的數據格式、內容等是否符合要求,對于不符合要求的數據是拋出異常還是暫時將這些異常數據加載到一張異常數據表中待后續處理等等都需要自己去實現,提前將不符合要求的數據清洗出去后,再對這些格式正確的數據進行加載并進行處理,這樣可以防止加工到最后一步發現數據有問題或者將錯誤數據加工出來出了報表,也可以防止因為臟數據導致后續批量中斷等問題。

在springbatch中,我們可以將數據校驗的過程寫成一個processor,在數據讀取后進行邏輯操作前進行數據校驗,將不合格的數據剔除或者暫存或者進行修復等。

下面我們新建AlipayValidateProcessor類,用于對支付寶賬單數據的校驗,這里實現的邏輯非常簡單,只是將金額字段小于0的作為異常拋出Exception并沒有做其他特殊處理,這里我們最好是自定義EXception,然后在配置Step的時候對自定義的Exception進行skip exception處理。

public class AlipayValidateProcessor implements ItemProcessor<AlipayTranDO, AlipayTranDO> {
    private static final Logger log = LoggerFactory.getLogger(AlipayValidateProcessor.class);

    @Override
    public AlipayTranDO process(AlipayTranDO alipayTranDO) throws Exception {
        if(Double.parseDouble(alipayTranDO.getAmount()) < 0){
            log.info("validate error: " + alipayTranDO.toString());
            throw new Exception();
        }else{
            return alipayTranDO;
        }
    }
}

下面就是要將數據校驗processor和數據加工processor多processor串聯執行,如果要串聯執行processor這就要用到CompositeItemProcessor類,將多個processor按順序加入list,并將processor list賦值給CompositeItemProcessor的delegete,將這個CompositeItemProcessor復合處理器作為整個step的processor,這樣在step執行的時候就會按照我們設置的復合processor先進行數據校驗清洗,再進行數據加工,這里有個地方要注意下,順序執行的processor的輸出類型對應的是下一個processor的輸入類型,第一個processor的輸入類型一定要是step的輸入類型,最后一個processor的輸出類型一定是step的輸出類型,例如例子中的step輸入為AlipayTranDO,輸出為HopPayTranDO,所以我們AlipayValidateProcessor的輸入為AlipayTranDO,輸出也為AlipayTranDO,AlipayItemProcessor的輸入為AlipayTranDO,輸出為HopPayTranDO。

public Step step3() {
  
        CompositeItemProcessor<AlipayTranDO,HopPayTranDO> compositeItemProcessor = new CompositeItemProcessor<AlipayTranDO,HopPayTranDO>();
        List compositeProcessors = new ArrayList();
        compositeProcessors.add(new AlipayValidateProcessor());
        compositeProcessors.add(new AlipayItemProcessor());
        compositeItemProcessor.setDelegates(compositeProcessors);
  
        return stepBuilderFactory.get("step3")
                .<AlipayTranDO, HopPayTranDO> chunk(10)
                .reader(alipayFileItemReader.getMultiAliReader())
                .processor(compositeItemProcessor)
                .writer(alipayFileItemWriter.getAlipayItemWriter())
                .build();
    }

3. 異常數據處理

對于在處理過程中如果遇到異常數據并且異常數據不是特別多的情況下,為了保證批量的順利執行,一般采取的做法是設置一個允許跳過的次數,這樣在遇到一些可以容忍的錯誤類型并行錯誤次數比較少的情況下就可以繼續執行,而不用中斷批量,做過運維的同學一定深有感觸,夜間批量中斷是多么痛苦的事情,第二天運維的同學一定會頂著黑眼圈找開發負責人去投訴批量中斷的事情,所以為了保證運維人員的身體健康,批量一定要有一定的異常數據容錯機制。springbatch支持設置skip的記錄最高次數,同樣也可以設置哪些錯誤可以跳過,哪些不能跳過,如果不設置那么只要批量執行有Exception,那就會中斷整個step。在springbatch中使用skip非常簡單,我們這個例子中對于異常沒有自定義,只是使用了Exception,這樣可以捕獲到大部分異常,實際使用過程中功能建議自定義Exception,這樣處理錯誤更有針對性,對于跳過的記錄,我們還需要設置一個SkipListener類用于監聽當出現跳過時回調進行處理的動作。SkipListener接口需要實現3個回調方法,開發者可以分別在這3個回調方法中實現相關跳過操作處理,例子中只是簡單記錄日志信息,我們也可以記錄到數據庫中,等待后續運維和運營查看數據是否有問題需要處理。

public class AlipaySkipListener implements SkipListener<AlipayTranDO, AlipayTranDO> {
    private static final Logger log = LoggerFactory.getLogger(AlipaySkipListener.class);

    @Override
    public void onSkipInProcess(AlipayTranDO alipayTranDO, Throwable throwable) {
        log.info("AlipayTran was skipped in process: "+alipayTranDO);
    }
    @Override
    public void onSkipInRead(Throwable arg0) {
    }
    @Override
    public void onSkipInWrite(AlipayTranDO alipayTranDO, Throwable throwable) {
        log.info("AlipayTran was skipped in process: "+alipayTranDO);
    }

}

除了可以設置skip我們還可以設置重試次數,例如我們需求中會去下載商戶的支付寶賬單,那么有可能因為網絡原因導致批量的時候某個商戶賬單無法下載下來,那么為了保證批量能夠執行下去,那么我們可以讓批量程序進行一定次數的重試,如果重試多次后還不行那么將進行跳過或報錯處理。

public Step step2() {
        return stepBuilderFactory.get("step2")
                .<AlipayTranDO, AlipayTranDO> chunk(10)
                .reader(alipayFileItemReader.getMultiAliReader())
                .writer(alipayDBItemWriter)
                .faultTolerant()
                .skipLimit(20)
                .skip(Exception.class)
                .listener(listener)
                .retryLimit(3)
                .retry(RuntimeException.class)
                .build();
    }

4. 并行配置

我們在日常處理批量的過程中,為了減少批量時間我們一般會將一些處理時間比較長的步驟并行執行充分利用系統資源,縮減批量執行時間,批量中一般有兩種并行方式,一種是對單個step多線程并行處理,這種適用于單step數據量特別大的情況,可以利用線程池多線程并行執行數據加工;還有一種是不同step沒有依賴關系并行處理,這種并行處理需要充分分析好這些并行step不存在資源爭奪,同時程序也是線程安全的,否則會出現很多資源競爭或者串數據的情況,我們這里只介紹單step多線程的實現方式。

需要和其他并發編程一樣,需要定義一個ThreadPoolExecutor

@Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setMaxPoolSize(4);
        taskExecutor.afterPropertiesSet();
        return taskExecutor;
    }

對需要并行的step設置taskExecutor就可以實現任務多線程并行執行了,是不是很簡單?

@Bean
    public Step step4() {
        return stepBuilderFactory.get("step3")
                .<AlipayTranDO, HopPayTranDO> chunk(10)
                .reader(alipayFileItemReader.getMultiAliReader())
                .processor(alipayItemProcessor)
                .writer(alipayFileItemWriter.getAlipayItemWriter())
                .taskExecutor(taskExecutor())
                .throttleLimit(4)
                .build();
    }

5. 定時自動任務配置

springbatch本身只是批量框架并沒有定時執行的功能,這里我們需要借助spring的schedule實現定時任務功能,做到批量無人值守自動執行,如果要更強大的功能可以使用Quartz來實現更加花樣百出的定時功能,這里我們需求沒那么復雜使用scheduler就能很好的完成所有功能,只需要一句注解,設置下cron屬性就可以了,cron屬性實例見下:

一個cron表達式有至少6個(也可能7個)有空格分隔的時間元素。
按順序依次為秒(0~59)、分鐘(0~59)、小時(0~23)、天(月)(0~31,但是你需要考慮你月的天數)、月(0~11)、天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)、年份(1970-2099)

0 0 10,14,16 * * ? 每天上午10點,下午2點,4點
0 0/30 9-17 * * ?   朝九晚五工作時間內每半小時
0 0 12 ? * WED 表示每個星期三中午12點 
"0 0 12 * * ?" 每天中午12點觸發 
"0 15 10 * * ?" 每天上午10:15觸發 
"0 * 14 * * ?" 在每天下午2點到下午2:59期間的每1分鐘觸發 
"0 0/5 14 * * ?" 在每天下午2點到下午2:55期間的每5分鐘觸發 
"0 0/5 14,18 * * ?" 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發 
"0 0-5 14 * * ?" 在每天下午2點到下午2:05期間的每1分鐘觸發 
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44觸發 
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15觸發 
"0 15 10 15 * ?" 每月15日上午10:15觸發 
"0 15 10 L * ?" 每月最后一日的上午10:15觸發 
"0 15 10 ? * 6L" 每月的最后一個星期五上午10:15觸發 
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一個星期五上午10:15觸發 
"0 15 10 ? * 6#3" 每月的第三個星期五上午10:15觸發 

除了使用cron屬性,也可以使用fixedRate來設定應用啟動后多久執行一次,單位為毫秒。

public class BillScheduler {
    @Autowired
    private BillBatchConfig billBatchConfig;

    private static final Logger log = LoggerFactory.getLogger(BillScheduler.class);

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Scheduled(initialDelay=10000, fixedRate = 10000)
    public void fixedBillBatch() {
        log.info("job begin {}", dateFormat.format(new Date()));
        billBatchConfig.run();
        log.info("job end {}", dateFormat.format(new Date()));
    }

    @Scheduled(cron="0 15 10 ? * *")
    public void fixedTimePerDayBillBatch() {
        log.info("job begin {}", dateFormat.format(new Date()));
        billBatchConfig.run();
        log.info("job end {}", dateFormat.format(new Date()));
    }
}

6. 批量監控設計

批量程序寫完了,批量監控也是很大的一塊內容,springbatch已經幫大家實現了大部分的監控功能,能夠對job執行當前情況和歷史情況進行監控并記錄,同時也可以對每個job的所有step執行情況進行監控,對step的順序也可以進行配置,所有的這一切功能都是依賴于springbatch的job repository模塊來實現的,因為工程中使用了mysql作為datasource那么只要springbatch第一次啟動的時候就會在datasource中新建這些用于批量監控的數據表:

Screenshot 2017-12-01 14.15.27.png

這些批量監控表的具體內容就由大家自己去研究吧,我這里就不一一贅述了,如果覺得springbatch監控做的不好,大家也可以自己去實現批量監控功能,自己實現批量監控的話就需要實現一些job和step的listener接口,編寫回調函數,在step和job執行成功后回調這些方法記錄執行情況。如果只是實現簡單批量那么自帶的監控已經夠用了。

小結

這篇springbatch的進階篇主要就是把日常批量程序中經常要用到的功能用springbatch javaconfig的方式實現了一把,對于剛接觸springbatch的同學會有很大幫助,同樣對于原來寫xml配置batch的同學轉javaconfig方式也有很大的幫助,代碼都放到了github上歡迎下載。

github:https://github.com/feiweiwei/BillSpringBatch.git

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容