跑批任務的處理思路


1 背景

合規要求將數據庫中的敏感用戶信息脫敏,賬號中心和賬戶中心的數據庫都有明文手機號。

2 解決思路

分兩部分看,存量數據和增量數據,其中增量數據要先處理。
增量數據,可以通過 Getter、Setter 來實現加解密。另外 Dao(Repository)可能包含 findByPhone 的查詢,需要調整為先根據密文查詢,如果結果為空,那么根據再明文查詢一遍。
存量數據,需要加密數據庫中存量的明文手機號,因為加密是一個CPU密集的操作,數據庫不適合做這種要動腦的處理,所以要寫Java定時任務來跑批。

3 第一版實現

定時任務線程作為主線程,如果只用主線程加密手機號,一批次1000個PO,每個PO get 出來一個明文,然后 set 回去,setter 負責加密,最后 save。
這樣一個批次處理1000個PO,實測耗時5s。嘗試過一批次處理10000個PO,實測耗時50s,雖然訪問數據庫的次數少了,但性能沒提升。
簡單做個算術,開發環境的賬號中心有一千萬的數據,全部跑下來需要 10000s,折合2.78小時。生產的數據是開發環境的好幾倍,這樣的效率肯定不行。

4 第二版實現

第二版做了兩個改進:

  1. 多線程處理。手機號加密是CPU密集型操作,我們機器的CPU資源有很大富余,所以用 Executors 創建了一個固定大小線程池,只負責加密,不負責插入等IO操作。
  2. 批量插入。開啟事務、提交事務都要走一次網絡IO和數據庫交互,本場景中有大量的短時間寫入操作,可以放到同一個事務中進行,減少開啟提交事務的開支。現通過 Future.isDone() 判斷當前批次脫敏完成后,一次性保存一個批次的記錄。但要注意這個事務不能太大,不然 redo_log 會炸掉,需要做一個權衡。

這個實現中,一個開始為了減少批量插入失敗回滾的損失,還做了個分片,就是把一個批次的記錄盡可能公平分派給各線程,有分片就會想到 work-steal (Java的實現是 ForkJoinPool),滿腦子的騷操作,最后還不如 FixedThreadPool 簡單高效。
在查詢明文手機號記錄的時候,sql 越跑越慢,是因為 LIMIT 0,100 要比 LIMIT 10000,100 快上不少,后者要遍歷 10000 個對象。最好別用。

4.1 線程池大小

Executors 創建出來的 FixedThreadPool,coreSize 和 maxSize 是一樣的。線上機器4個單核CPU,UAT由於是多服務部署,有8個單核CPU,每個環境都不一樣,需要動態設置 maxSize,這里 maxSize 設置為 Runtime.getRuntime().availableProcessors() * 3。

4.2 一個批次處理多少數據

FixedThreadPool 用的 LinkedBlockingQueue 是一個無界隊列,稍不注意會炸掉JVM,因此需要謹慎評估內存占用。
跑個 show table 觀察一下單條記錄多大:

SHOW TABLE STATUS WHERE `name` = 't_user'

看到 Index_length為409600(b),Rows為1655,那么1條記錄就是247(b),加載到內存中會更大,假設300(b)。(只能算個大概,看內存占用建議用 MAT)
在本地開發機器上執行 Runtime.getRuntime().freeMemory() 得到 225973632(b),一批次可以處理753245條數據,內存是足夠了。
數據庫層面上,phone這個字段帶有二級索引(KEY),但由於查詢的時候用到了函數 length(phone),對索引列使用函數會導致索引失效,進而全表掃描,所以查詢壓力較大。
上文說到,大事務會撐爆 redo_log,哪怕不考慮IO性能,一個事務的批量插入都不能太大。
最后很慫地一個批次跑 20000 條數據,記住 jdbc參數 開啟 rewriteBatchedStatements。

Reference

[1] limit查詢慢的原因及優化方法
[2] 一個Java對象到底占用多大內存?
[3] MySQL之rewriteBatchedStatements


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM