大家好,我是田螺。
相信很多后端開發的伙伴們,都做過刷數任務了吧。今天跟大家聊聊,做好一個刷數任務,需要具備哪些后端思維。
我們做刷數任務的時候,首先要考慮的是,這些被刷的數據是否還要還原的。或者刷出問題時,需要回滾的。如果是的話,我們就要做好備份。
如果你是把數據遷移到新的表,則有可能不需要備份,這個具體問題具體分析的哈。
通常,我們在一個事務內,先備份數據,再操作刷數邏輯。
圖片
當然,備份數據的方式有多種方式,可以數據庫備份,比如搞一個備份表。或者文件系統快照等,在需要的時候,就還原數據。
我們刷數的時候,先確認下具體的業務需求和數據模型。然后需要確定刷數的維度是什么。
圖片
當然,還有其他維度,比如產品維度等等都可以,就看業務需求和你們的數據模型。
確認了刷數維度后,需要思考你的刷數是否支持灰度。比如確認客戶維度刷數后,你實現的代碼,是否支持灰度刷數,也就是說,是否支持先刷一部分客戶,確認沒問題后,再根據配置刷全量客戶。
比如,你要給一個客戶相關的業務表刷數,需要考慮并發場景,簡單點說,就是你在刷數的過程中,客戶是否可能在做交易請求,這時候,是否可能產生臟數據。
一般情況下,可以考慮給客戶維度加分布式鎖,比如加鎖的key是customerId。但是我們加鎖的時候,肯定是不希望影響客戶太久的,因為加鎖后,整個刷數的過程中,系統是處理不了這個用戶的交易請求的,也就是說影響交易了。
圖片
所以也一般刷數任務,我們會選擇在夜深人靜的時候執行,這時候用戶發生交易的概率很低,影響相對較小。
我們在刷數的時候,有可能會刷失敗。比如因為網絡問題、或者目標表結構等等。失敗后,我們有哪些措施保證呢:
圖片
如果刷數失敗的話,我們要確保數據的完整性和一致性,一般一個刷數維度,需要加事務,確保失敗可以回滾,保證數據一致性。刷失敗之后,首先確認分布式鎖要被移除(如果有加鎖的話),因為要確認即使失敗后,交易也是能正常進行的。
還要考慮失敗支持重試。可以自動重試或者手動重試,比如通過xxl-job定時任務撈取,繼續重試。有時候,可以設置重試次數和重試間隔,確保任務在一段時間內嘗試恢復。
我們第4小節提到,如果失敗了,需要保證數據的完整性和一致性。其實,一般我們刷數,就是通過加事務處理去保證的。
事務需要加到恰到好處,比如我們不能所有的刷數業務都放到一個事務內。如果我們是按照客戶維度來刷數的,我們就一個事務把一個客戶所有的數據刷的邏輯放到一塊,當然,有些查詢是可以放到事務外處理的,一些更新或者修改、刪除操作則放到事務內。
圖片
如果你的數據是分庫處理的,則有可能刷數的時候要考慮分布式事務了。
如果我們的刷數任務數據量很大,執行耗時比較久的話。就建議可以多線程并行執行。
比如你是分庫分表的,是有30個表,你可以A線程執行1-10的表,B線程執行11-20的表,C線程執行21-30的表。
圖片
我們執行一個刷數任務,一定要做好日志記錄。
我記得我們技術領導說過一句話,很有道理:評價你的日志是否打印得是否夠好。就是你根據控制臺打印出的日志,能知道你的復雜業務執行到什么流程。如果異常中斷,你能根據日志快速知道什么異常,哪個業務數據有問題,那就夠了。
刷數日志打印,一般包括:
我們開發刷數邏輯的時候,如果某種返回不符合預期的時候,就需要告警上報監控(比如插入數據庫返回跟預期插入條數不一致)。
又或者是你刷數失敗,需要包這個異常日志打印出來,并且上報監控(比如普羅米修斯,和企業微信通知)。比如這塊代碼:
try{ flushService.flushDataCustomerLevel(customerNo);}catch(Exception e){ Logger.error("flush customer data fail: {}", customerNo, e); prometheusMonitor.report("刷數失敗",customerNo); notify(); weChatWorkSend();}
如果你刷的數據量很大的時候,最好做壓測。壓測通常包括模擬多種負載情況,以確保系統在不同條件下都能正常運行。
做刷數任務壓測,主要考慮這幾方面:
刷數壓測的好處:
圖片
有些時候,我們如果擔心刷數任務跑太久,可能會影響交易,這時候我們可以搞個配置變量,比如apollo配置變量,控制刷數多長時間后,可以停止。
我們做刷數任務的時候,經常是分頁循環掃描某個客戶/用戶表,然后一批一批出來執行刷數邏輯。比如偽代碼像這樣:
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(); }
這塊代碼,其實沒啥問題。有些時候,我們可能手抖寫錯了,可能導致死循環。
其實為了保護我們的系統,我們可以先確認下客戶有多少,然后設置個循環次數,當超過最大循環次數之后,就告警排查確認。
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 ++; }
當然這只是個一種后端思路哈。
大家如果使用過xxl-job作為定時任務,應該抖配置過它的路由規則吧。比如是分片的,還是第一個/最后一個等等。
如果是分片的,就是多個pod都可能執行到你的業務邏輯。這時候你要考慮并發執行,你的業務是否收影響。
我們做刷數任務的時候,很多時候,都要跟SQL打交道。
我們要確保查詢、更新、或者刪除的數據量大的表,都要有索引了。要確保沒有慢SQL。
常規的我們可以用explain分析SQL,我們還可以通過壓測分析出來。
如果你是按照客戶維度刷數,加了客戶維度的分布式鎖,你要考慮鎖時間是多久,鎖時間是否可以配置(一般這種最好配置一下。)
如果你時間設置小,那這時候刷數還沒完成,鎖就超時釋放了,那不就有問題啦。
如果你時間設置過長也不太好,當然,你在刷完數,finally執行釋放鎖也可以。
finally { redisService.deleteKey(customerNoKey); }
我們在刷數的時候,為了保證數據的完整性和一致性,一般要求加事務的。
但是,切忌事務不要太大,我們可以把一些查詢放到事務外,把計算邏輯也放事務外,把數據庫的更新、新增、刪除操作放到事務內就好。
就是把大事務拆分為小事務。
一般來說,做刷數,盡量打印一下耗時,這樣我們可以根據日志,觀察是否有哪些問題需要及時處理的。
比如打印刷一個客戶要多久。或者打印一批客戶要多久,等等。
有些時候,我們需要配置一定的灰度規則來支持灰度刷數。如果刷數流程是先掃描所有客戶,然后接著判斷客戶是否命中灰度。這樣每次任務執行,都會掃描客戶表。
我么可以考慮加個配置,傳特定的客戶號,根據客戶號列表,去查客戶列表,然后開始刷數邏輯,不用再全表掃描客戶表了。
有些時候,我們沒法確認我們刷的邏輯是否正確,這時候,可以考慮是否加校驗邏輯。
你可以異步進行校驗,也可以同步校驗(當然,如果耗時不大的時候)
如果我們是按照一個客戶維度去刷數的,你要確認A客戶刷失敗,是不是不能影響B客戶。這時候建議try...catch 包住可能的異常,這樣即有利于分析錯誤原因,又可以不不因為未知異常導致刷數中斷。
本文鏈接:http://www.www897cc.com/showinfo-26-76543-0.html實現一個刷數任務,需要思考哪些維度?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 用 Python 優雅地玩轉 Elasticsearch:實用技巧與最佳實踐
下一篇: 垃圾收集器的秘密:深入理解JVM性能調優