實(shí)現(xiàn)一個(gè)刷數(shù)任務(wù),需要思考哪些維度?
前言
大家好,我是田螺。
相信很多后端開發(fā)的伙伴們,都做過(guò)刷數(shù)任務(wù)了吧。今天跟大家聊聊,做好一個(gè)刷數(shù)任務(wù),需要具備哪些后端思維。
1. 數(shù)據(jù)的備份和還原
我們做刷數(shù)任務(wù)的時(shí)候,首先要考慮的是,這些被刷的數(shù)據(jù)是否還要還原的。或者刷出問(wèn)題時(shí),需要回滾的。如果是的話,我們就要做好備份。
如果你是把數(shù)據(jù)遷移到新的表,則有可能不需要備份,這個(gè)具體問(wèn)題具體分析的哈。
通常,我們?cè)谝粋€(gè)事務(wù)內(nèi),先備份數(shù)據(jù),再操作刷數(shù)邏輯。
圖片
當(dāng)然,備份數(shù)據(jù)的方式有多種方式,可以數(shù)據(jù)庫(kù)備份,比如搞一個(gè)備份表?;蛘呶募到y(tǒng)快照等,在需要的時(shí)候,就還原數(shù)據(jù)。
2.刷數(shù)維度是什么?是否支持灰度?
我們刷數(shù)的時(shí)候,先確認(rèn)下具體的業(yè)務(wù)需求和數(shù)據(jù)模型。然后需要確定刷數(shù)的維度是什么。
圖片
- 客戶維度:比如刷數(shù)的維度可以是一個(gè)客戶。刷數(shù)的時(shí)候,把這個(gè)客戶的所有數(shù)據(jù)撈取出來(lái),在一個(gè)事務(wù)內(nèi),把客戶的所有數(shù)據(jù),按照業(yè)務(wù)的規(guī)則,刷到一個(gè)表。
- 賬號(hào)維度:刷數(shù)的維度是賬號(hào),在執(zhí)行刷數(shù)的時(shí)候,就是把這個(gè)賬號(hào)的所有數(shù)據(jù)撈取出來(lái),在一個(gè)事務(wù)內(nèi),把賬號(hào)的所有數(shù)據(jù),刷到另外一個(gè)表。
當(dāng)然,還有其他維度,比如產(chǎn)品維度等等都可以,就看業(yè)務(wù)需求和你們的數(shù)據(jù)模型。
確認(rèn)了刷數(shù)維度后,需要思考你的刷數(shù)是否支持灰度。比如確認(rèn)客戶維度刷數(shù)后,你實(shí)現(xiàn)的代碼,是否支持灰度刷數(shù),也就是說(shuō),是否支持先刷一部分客戶,確認(rèn)沒問(wèn)題后,再根據(jù)配置刷全量客戶。
3. 并發(fā)考慮,刷數(shù)過(guò)程是否需要加鎖。
比如,你要給一個(gè)客戶相關(guān)的業(yè)務(wù)表刷數(shù),需要考慮并發(fā)場(chǎng)景,簡(jiǎn)單點(diǎn)說(shuō),就是你在刷數(shù)的過(guò)程中,客戶是否可能在做交易請(qǐng)求,這時(shí)候,是否可能產(chǎn)生臟數(shù)據(jù)。
一般情況下,可以考慮給客戶維度加分布式鎖,比如加鎖的key是customerId。但是我們加鎖的時(shí)候,肯定是不希望影響客戶太久的,因?yàn)榧渔i后,整個(gè)刷數(shù)的過(guò)程中,系統(tǒng)是處理不了這個(gè)用戶的交易請(qǐng)求的,也就是說(shuō)影響交易了。
圖片
所以也一般刷數(shù)任務(wù),我們會(huì)選擇在夜深人靜的時(shí)候執(zhí)行,這時(shí)候用戶發(fā)生交易的概率很低,影響相對(duì)較小。
4. 刷數(shù)失敗怎么辦?是否支持重試?
我們?cè)谒?shù)的時(shí)候,有可能會(huì)刷失敗。比如因?yàn)榫W(wǎng)絡(luò)問(wèn)題、或者目標(biāo)表結(jié)構(gòu)等等。失敗后,我們有哪些措施保證呢:
圖片
如果刷數(shù)失敗的話,我們要確保數(shù)據(jù)的完整性和一致性,一般一個(gè)刷數(shù)維度,需要加事務(wù),確保失敗可以回滾,保證數(shù)據(jù)一致性。刷失敗之后,首先確認(rèn)分布式鎖要被移除(如果有加鎖的話),因?yàn)橐_認(rèn)即使失敗后,交易也是能正常進(jìn)行的。
還要考慮失敗支持重試。可以自動(dòng)重試或者手動(dòng)重試,比如通過(guò)xxl-job定時(shí)任務(wù)撈取,繼續(xù)重試。有時(shí)候,可以設(shè)置重試次數(shù)和重試間隔,確保任務(wù)在一段時(shí)間內(nèi)嘗試恢復(fù)。
5. 恰到好處的事務(wù)處理
我們第4小節(jié)提到,如果失敗了,需要保證數(shù)據(jù)的完整性和一致性。其實(shí),一般我們刷數(shù),就是通過(guò)加事務(wù)處理去保證的。
事務(wù)需要加到恰到好處,比如我們不能所有的刷數(shù)業(yè)務(wù)都放到一個(gè)事務(wù)內(nèi)。如果我們是按照客戶維度來(lái)刷數(shù)的,我們就一個(gè)事務(wù)把一個(gè)客戶所有的數(shù)據(jù)刷的邏輯放到一塊,當(dāng)然,有些查詢是可以放到事務(wù)外處理的,一些更新或者修改、刪除操作則放到事務(wù)內(nèi)。
圖片
如果你的數(shù)據(jù)是分庫(kù)處理的,則有可能刷數(shù)的時(shí)候要考慮分布式事務(wù)了。
6. 性能優(yōu)化,考慮多線并行執(zhí)行。
如果我們的刷數(shù)任務(wù)數(shù)據(jù)量很大,執(zhí)行耗時(shí)比較久的話。就建議可以多線程并行執(zhí)行。
比如你是分庫(kù)分表的,是有30個(gè)表,你可以A線程執(zhí)行1-10的表,B線程執(zhí)行11-20的表,C線程執(zhí)行21-30的表。
圖片
7. 日志記錄
我們執(zhí)行一個(gè)刷數(shù)任務(wù),一定要做好日志記錄。
我記得我們技術(shù)領(lǐng)導(dǎo)說(shuō)過(guò)一句話,很有道理:評(píng)價(jià)你的日志是否打印得是否夠好。就是你根據(jù)控制臺(tái)打印出的日志,能知道你的復(fù)雜業(yè)務(wù)執(zhí)行到什么流程。如果異常中斷,你能根據(jù)日志快速知道什么異常,哪個(gè)業(yè)務(wù)數(shù)據(jù)有問(wèn)題,那就夠了。
刷數(shù)日志打印,一般包括:
- 記錄詳細(xì)的刷數(shù)任務(wù)日志
- 包括執(zhí)行的步驟
- 刷數(shù)成功與否
- 刷數(shù)耗時(shí)等信息
8.監(jiān)控告警
我們開發(fā)刷數(shù)邏輯的時(shí)候,如果某種返回不符合預(yù)期的時(shí)候,就需要告警上報(bào)監(jiān)控(比如插入數(shù)據(jù)庫(kù)返回跟預(yù)期插入條數(shù)不一致)。
又或者是你刷數(shù)失敗,需要包這個(gè)異常日志打印出來(lái),并且上報(bào)監(jiān)控(比如普羅米修斯,和企業(yè)微信通知)。比如這塊代碼:
try{
flushService.flushDataCustomerLevel(customerNo);
}catch(Exception e){
Logger.error("flush customer data fail: {}", customerNo, e);
prometheusMonitor.report("刷數(shù)失敗",customerNo);
notify();
weChatWorkSend();
}
9. 數(shù)據(jù)量大的時(shí)候,最好壓測(cè)
如果你刷的數(shù)據(jù)量很大的時(shí)候,最好做壓測(cè)。壓測(cè)通常包括模擬多種負(fù)載情況,以確保系統(tǒng)在不同條件下都能正常運(yùn)行。
做刷數(shù)任務(wù)壓測(cè),主要考慮這幾方面:
- 數(shù)據(jù)量:使用大量數(shù)據(jù)進(jìn)行測(cè)試,以確保系統(tǒng)在處理大規(guī)模數(shù)據(jù)時(shí)的性能。這可以包括數(shù)據(jù)量的增長(zhǎng)、查詢和寫入的吞吐量等。
- 網(wǎng)絡(luò)延遲和吞吐量:在模擬網(wǎng)絡(luò)延遲和限制吞吐量的情況下進(jìn)行測(cè)試,以了解系統(tǒng)在這些條件下的表現(xiàn)。
- 錯(cuò)誤處理:引入模擬的錯(cuò)誤場(chǎng)景,例如數(shù)據(jù)庫(kù)連接中斷、請(qǐng)求超時(shí)等,以驗(yàn)證系統(tǒng)對(duì)錯(cuò)誤的處理能力。
刷數(shù)壓測(cè)的好處:
圖片
- 性能評(píng)估:壓測(cè)可以幫助評(píng)估系統(tǒng)在處理大量數(shù)據(jù)時(shí)的性能。通過(guò)模擬真實(shí)負(fù)載,可以更好地了解系統(tǒng)的響應(yīng)時(shí)間、吞吐量和資源利用情況。
- 容量規(guī)劃:通過(guò)壓測(cè),可以更好地了解系統(tǒng)在不同負(fù)載條件下的容量需求。這有助于進(jìn)行容量規(guī)劃,確保系統(tǒng)能夠應(yīng)對(duì)未來(lái)的增長(zhǎng)。
- 發(fā)現(xiàn)潛在問(wèn)題:壓測(cè)可以幫助發(fā)現(xiàn)系統(tǒng)中可能存在的潛在問(wèn)題,如內(nèi)存泄漏、并發(fā)問(wèn)題、資源瓶頸等。通過(guò)在模擬環(huán)境中發(fā)現(xiàn)并解決這些問(wèn)題,可以避免它們?cè)谏a(chǎn)環(huán)境中引起嚴(yán)重后果。
- 驗(yàn)證容錯(cuò)能力:壓測(cè)可以測(cè)試系統(tǒng)在出現(xiàn)錯(cuò)誤、超時(shí)、網(wǎng)絡(luò)故障等異常條件下的容錯(cuò)能力。這有助于確保系統(tǒng)能夠適應(yīng)不可預(yù)測(cè)的情況。
- 性能優(yōu)化:壓測(cè)結(jié)果提供了性能瓶頸的線索,幫助團(tuán)隊(duì)進(jìn)行性能優(yōu)化。通過(guò)分析性能數(shù)據(jù),可以識(shí)別需要改進(jìn)的部分并優(yōu)化系統(tǒng)。
- 規(guī)遇風(fēng)險(xiǎn):通過(guò)壓測(cè),可以更早地發(fā)現(xiàn)潛在的性能問(wèn)題和瓶頸,有助于規(guī)遇系統(tǒng)上線后可能面臨的風(fēng)險(xiǎn)。
10. 實(shí)戰(zhàn)壓軸匯總:做一個(gè)刷數(shù)任務(wù),如何更好保護(hù)你的系統(tǒng)
10.1 設(shè)置個(gè)配置時(shí)間,可以控制任務(wù)跑多長(zhǎng)時(shí)間后終止。
有些時(shí)候,我們?nèi)绻麚?dān)心刷數(shù)任務(wù)跑太久,可能會(huì)影響交易,這時(shí)候我們可以搞個(gè)配置變量,比如apollo配置變量,控制刷數(shù)多長(zhǎng)時(shí)間后,可以停止。
10.2 循環(huán)分頁(yè)查詢,設(shè)置最大次數(shù)告警監(jiān)控
我們做刷數(shù)任務(wù)的時(shí)候,經(jīng)常是分頁(yè)循環(huán)掃描某個(gè)客戶/用戶表,然后一批一批出來(lái)執(zhí)行刷數(shù)邏輯。比如偽代碼像這樣:
long minId
while(true){
List<CustomerDo> customerList = customer.pageQueryAscID(pageSize,minId);
flushCustomerData(customerList);
if(customerList.size()< pageSize){
break;
}
minId = customerList.get(customerList.size()-1).getId();
}
這塊代碼,其實(shí)沒啥問(wèn)題。有些時(shí)候,我們可能手抖寫錯(cuò)了,可能導(dǎo)致死循環(huán)。
其實(shí)為了保護(hù)我們的系統(tǒng),我們可以先確認(rèn)下客戶有多少,然后設(shè)置個(gè)循環(huán)次數(shù),當(dāng)超過(guò)最大循環(huán)次數(shù)之后,就告警排查確認(rèn)。
long minId;
Integer maxCycleNum =1000;
Integer cycleNum = 0;
while(true ){
List<CustomerDo> customerList = customer.pageQueryAscID(pageSize,minId);
flushCustomerData(customerList);
if(customerList.size()< pageSize){
break;
}
minId = customerList.get(customerList.size()-1).getId();
if(cycleNum>= maxCycleNum){
//告警
}
cycleNum ++;
}
當(dāng)然這只是個(gè)一種后端思路哈。
10.3 如果定時(shí)任務(wù)是xxl-job ,路由規(guī)則是什么,并發(fā)問(wèn)題考慮
大家如果使用過(guò)xxl-job作為定時(shí)任務(wù),應(yīng)該抖配置過(guò)它的路由規(guī)則吧。比如是分片的,還是第一個(gè)/最后一個(gè)等等。
如果是分片的,就是多個(gè)pod都可能執(zhí)行到你的業(yè)務(wù)邏輯。這時(shí)候你要考慮并發(fā)執(zhí)行,你的業(yè)務(wù)是否收影響。
10.4 SQL是否命中索引,是否存在慢SQL
我們做刷數(shù)任務(wù)的時(shí)候,很多時(shí)候,都要跟SQL打交道。
我們要確保查詢、更新、或者刪除的數(shù)據(jù)量大的表,都要有索引了。要確保沒有慢SQL。
常規(guī)的我們可以用explain分析SQL,我們還可以通過(guò)壓測(cè)分析出來(lái)。
10.5 如果加了分布式鎖,鎖時(shí)間考慮
如果你是按照客戶維度刷數(shù),加了客戶維度的分布式鎖,你要考慮鎖時(shí)間是多久,鎖時(shí)間是否可以配置(一般這種最好配置一下。)
如果你時(shí)間設(shè)置小,那這時(shí)候刷數(shù)還沒完成,鎖就超時(shí)釋放了,那不就有問(wèn)題啦。
如果你時(shí)間設(shè)置過(guò)長(zhǎng)也不太好,當(dāng)然,你在刷完數(shù),finally執(zhí)行釋放鎖也可以。
finally {
redisService.deleteKey(customerNoKey);
}
10.6 大事務(wù)考慮,事務(wù)是否太多?是否可以拆分為小事務(wù)
我們?cè)谒?shù)的時(shí)候,為了保證數(shù)據(jù)的完整性和一致性,一般要求加事務(wù)的。
但是,切忌事務(wù)不要太大,我們可以把一些查詢放到事務(wù)外,把計(jì)算邏輯也放事務(wù)外,把數(shù)據(jù)庫(kù)的更新、新增、刪除操作放到事務(wù)內(nèi)就好。
就是把大事務(wù)拆分為小事務(wù)。
10.7 打印耗時(shí)時(shí)間,如果時(shí)間夠長(zhǎng)
一般來(lái)說(shuō),做刷數(shù),盡量打印一下耗時(shí),這樣我們可以根據(jù)日志,觀察是否有哪些問(wèn)題需要及時(shí)處理的。
比如打印刷一個(gè)客戶要多久?;蛘叽蛴∫慌蛻粢嗑茫鹊?。
10.8 是否可以加個(gè)配置,減少掃描
有些時(shí)候,我們需要配置一定的灰度規(guī)則來(lái)支持灰度刷數(shù)。如果刷數(shù)流程是先掃描所有客戶,然后接著判斷客戶是否命中灰度。這樣每次任務(wù)執(zhí)行,都會(huì)掃描客戶表。
我么可以考慮加個(gè)配置,傳特定的客戶號(hào),根據(jù)客戶號(hào)列表,去查客戶列表,然后開始刷數(shù)邏輯,不用再全表掃描客戶表了。
10.9 是否考慮校驗(yàn)邏輯
有些時(shí)候,我們沒法確認(rèn)我們刷的邏輯是否正確,這時(shí)候,可以考慮是否加校驗(yàn)邏輯。
你可以異步進(jìn)行校驗(yàn),也可以同步校驗(yàn)(當(dāng)然,如果耗時(shí)不大的時(shí)候)
10.10 try...catch 包住可能的異常
如果我們是按照一個(gè)客戶維度去刷數(shù)的,你要確認(rèn)A客戶刷失敗,是不是不能影響B(tài)客戶。這時(shí)候建議try...catch 包住可能的異常,這樣即有利于分析錯(cuò)誤原因,又可以不不因?yàn)槲粗惓?dǎo)致刷數(shù)中斷。