互聯網那些事之數據丟失

来源:https://www.cnblogs.com/evan-liang/archive/2020/01/23/12233902.html

互聯網那些事之數據丟失 本系列故事的所有案例和解決方案只是筆者以前在互聯網工作期間的一些事例,僅供大家參考,實際操作應該根據業務和項目情況設計,歡迎大家留言提出寶貴的意見 背景 小王和小明分別維護分散式系統中A、b兩個服務,有一個場景是 A服務會向B服務通過MQ發送事件並且推送用戶信息,然後B服務保 ...


 

互聯網那些事之數據丟失

本系列故事的所有案例和解決方案只是筆者以前在互聯網工作期間的一些事例,僅供大家參考,實際操作應該根據業務和項目情況設計,歡迎大家留言提出寶貴的意見

背景

小王和小明分別維護分散式系統中A、b兩個服務,有一個場景是 A服務會向B服務通過MQ發送事件並且推送用戶信息,然後B服務保存用戶信息。
在這裡插入圖片描述

有一天,小王和小明因為一件事討論得熱火朝天、互不相讓,事情由來如下:

  • 風控部的童鞋找小明說在B服務的資料庫找不到一些用戶資料
  • 小明經過排查,B服務表裡確實沒有這批用戶的數據,在日誌里偶爾看到了一些Redis連接超時異常,小明想小王手動幫忙重推試試
  • 小王經過排查,確保自己已經成功推送了那幾個用戶的數據,並且推送的時候A服務並沒有發現MQ異常,覺得自己沒有義務去幫忙重推,應該小明自己解決

這時候,在一旁掃地的清潔工老梁過來調解,並幫忙排查分析,導致這個問題的主要原因如下:

  • B服務在接受MQ的處理類捕獲了異常,因為異常並沒有拋出,所以框架預設自動回覆了ACK,MQ認為已經消費者處理成功,就不再重覆投放到隊列,但此時方法體內因為工具包出現Redis連接超時,拋出異常,導致消息並沒有被正常處理

偽代碼如下:

    @RabbitHandler
    public void handle(byte[] message) {
        try {
            t = parseBody(messageStr);
        } catch (Exception e) {
            log.error("消費消息失敗", e.getCause());
        }
    }

    private void handleMessage(T t) throws MQHandleException {
	    //唯一標識
        String key = t.getLockedId();
        //獲取鎖
        DistributedLock lock = DistributedLockFactory.getLock(key);
        try {
            // 解決分散式服務提交相同資料併發問題
            lock.lock(CacheConstants.LOCK_WAIT_TIME, CacheConstants.LOCK_LEASE_TIME, CacheConstants.DEFAULT_CACHE_UNIT);
            // 處理業務邏輯
            handleBusinessLogic(t);
        } catch (LockException e) {
            throw new MQHandleException(e);
        } finally {
            // 釋放鎖
            lock.unLock();
        }
    }

  • 頻繁Redis超時是因為A、B服務共用一個Redis,A服務Key太多把Redis記憶體資源占滿了(也可能連接占滿),導致了B服務經常出現連接超時(該故障不是本章主要關註目標)

  • B服務在已經成功接受到消息後,沒有把消息先保存起來,所以也導致了自身並沒有能力重跑

清潔工老梁跟小王和小明進行一番詳談後,瞭解到他們主要需求有兩個:

  • B服務儘可能自己重新消費信息,而不是一昧依賴A服務手動重推
  • B服務對已接收到的消息,能自己重新消費,當然,這裡指的是有意義的消息,如果一些本身A服務推送過來的消息就是有問題的,例如格式錯誤之類的,這些B服務可以要求A重推

解決思路

經過上面的分析,老梁的解題思路主要分為兩個方向:

  • B服務建立自己的本地異常消息事件表。
  • B服務做異常分類,只對可以重跑的消息事件進行重跑

本地異常消息事件表

一般來說,常見的微服務架構實現最終一致性有三種模式:可靠事件模式、業務補償模式、TCC模式。這裡AB服務是通過業務補償模式實現最終一致性,但這裡又跟我們一般的分散式架構的事務問題不同,這裡我們只需要保證B服務能最終把正常消息事件消費成功即可。

實現思路:

  • 建立一張本地異常消息事件表,為了避免太多資料庫IO操作,這裡只會記錄異常事件
  • 提取一個通用消息處理層,統一保存異常消息事件,併進行狀態更新
  • 提取一個事件恢復模塊,統一對失敗事件進行追蹤
  • 對於重跑仍失敗消息事件,設置一個重跑次數上限,進行自動重跑,可以通過調度任務去做(事件恢復模塊),當重跑多次仍然失敗(像網路異常和資料庫異常之類,短時間不會被修複),則後期進行人工重跑

表設計


針對於B服務,對於收到的MQ信息沒有進行有效的記錄,而且MQ信息處理之後,存在修改錯誤,沒法進行對應信息補充修複的功能,增加通用消息處理層,進行消息體的記錄和回溯。 在獲取消息之後進行一次記錄,進行冪等操作和對應的狀態更新, 消息狀態在業務相關操作完成後,標記為處理完成,認為對應消息狀態結束。

這裡hash_value是對請求體進行hash計算得出來的一個值,例如:MD5、SHA-2,保證每個不同請求的hash碼不一樣,相同的請求hash碼相同,可以用於冪等控制。

表大致操作流程:

異常消息狀態設計

異常消息有4個狀態

  • 待處理 當系統消費失敗時,會對特定的異常插入異常事件表,初始狀態為 待處理
  • 處理中 當失敗恢復模塊開始執行任務時會把當前異常事件狀態設置為 處理中
  • 處理完成 當失敗事件重跑成功後,會把當前異常事件狀態設置為 處理完成
  • 異常 當失敗事件重跑超過上限次數後,會把當前異常事件狀態設置為 異常,等待後期人工重跑

事件恢復模塊

失敗事件隊列在這裡是採用資料庫表代替


異常分類

因為並非所有的異常都能重跑就能解決問題,我們只能針對可以修複的異常進行重試,這裡把異常分為兩大類:

  • 可修複異常:可修複異常指的是可以通過重跑解決的異常,如:資料庫超時、資料庫缺少欄位、Redis獲取鎖失敗、處理邏輯有問題導致信息缺失、系統升級導致消費失敗、網路問題、伺服器不穩定等引起。
    • 可立即修複異常:指一些可以通過立即重試就能恢復的異常。例如短暫的網路中斷引起的異常,一般可以在功能代碼級進行立即重試,可以使用spring-retry等組件
    • 延遲修複異常:指一些短時間內不能立即恢復的異常,需要延遲執行,等待故障修複。例如依賴的下游系統正在升級,導致一段時間服務介面中斷不可以用,需要等待服務啟動才能使用,一般通過定時任務設定一定時間間隔或者重跑次數去解決
    • 人工修複異常:指系統沒辦法直接修複,出現了一些未知異常或者短時間內不可解決的異常,例如Redis宕掉無法預知修複時間、上線時腳本遺漏導致表裡缺少欄位等,需要人工干預進行重跑,一般通過後臺管理頁面操作
  • 不可修複異常:不可修複異常指不能通過重跑就能解決的異常。如:上游系統傳輸格式有問題、消息事件內容本身有誤等引起的異常,這些即使重跑也解決不了問題,應該要從上游系統或者根源去解決。

B服務異常處理流程

最後小明負責的B服務按照老梁的思路,重新調整了代碼,異常處理流程如下:

 


您的分享是我們最大的動力!

更多相關文章
  • "正則表達式知識" 利用模式匹配,幾行代碼搞定 ...
  • 講究一個對稱性 javascript export default (n) = { // 遞歸函數,用來算輸入為n的格雷編碼序列 let make = (n) = { if (n === 1) { return ['0', '1'] } else { // 獲取上一回的結果 let prev = m ...
  • ![](https://img2018.cnblogs.com/blog/1853166/202001/1853166-20200126143013655-154923141.png) ![](https://img2018.cnblogs.com/blog/1853166/202001/18531... ...
  • 滿足最小分組的長度為其他分組的整數倍 ...
  • ![](https://img2018.cnblogs.com/blog/1853166/202001/1853166-20200126115527094-488420466.png)![](https://img2018.cnblogs.com/blog/1853166/202001/185316... ...
  • jQuery 滑動方法 通過 jQuery,您可以在元素上創建滑動效果。 jQuery 擁有以下滑動方法: slideDown() slideUp() slideToggle() jQuery slideDown() 方法 jQuery slideDown() 方法用於向下滑動元素。 語法: $(s ...
  • javascript export default (str) = { // 建立數據結構,堆棧,保存數據 let r = [] // 給定任意子輸入都返回第一個符合條件的子串 let match = (str) = { let j = str.match(/^(0+|1+)/)[0] let o ...
  • 第一種 第二種 第三種 第四種 ...
一周排行
  • 《ASP.NET MVC 企業級實戰》 [作者] (中) 鄒瓊俊[出版] 清華大學出版社[版次] 2017年04月 第1版[印次] 2019年08月 第6次 印刷[定價] 89.00元 【第01章】 (P021) 只有在 Lambda 有一個輸入參數時,括弧才是可選的,否則括弧是必需的。 使用空括弧 ...
  • 上一篇(https://www.cnblogs.com/meowv/p/12971041.html)使用HtmlAgilityPack抓取壁紙數據成功將圖片存入資料庫,本篇繼續來完成一個全網各大平臺的熱點新聞數據的抓取。 同樣的,可以先預覽一下我個人博客中的成品:https://meowv.com/ ...
  • 前言 請了一天假後回公司,同事跟我說使用Newtonsoft.json序列化TreeView對象的時候出現報錯; 啊!什麼?這個類庫不是能夠序列化所有東西嗎?真的很懵逼,也是我第一次使用這個類庫出現問題! 問題異常 異常信息 : Newtonsoft.Json.JsonSerializationEx ...
  • 簡單瞭解下麵詞語的意思 節點:二叉樹中每個元素都稱為節點 葉子節點(簡稱:葉子):度為0的節點,葉子節點就是樹中最底段的節點,葉子節點沒有子節點,也叫終端結點 分枝節點:度不為0的結點 節點的度:二叉樹的度代表某個節點的孩子或者說直接後繼的個數,簡單說就是一個節點擁有的子樹數 樹的度: 樹中最大的結 ...
  • C# 中的LINQ 提供了兩種操作方式,查詢表達式和查詢操作符,所有的查詢表達式都有對應的查操作符類替代,查詢表達式有點“類” SQL,在代碼中寫SQL,總覺得不夠“優雅”,使用查詢操作符就顯得“優雅”很多, 本系列就來對所有的LINQ 標準操作符進行一個全面的總結,這些操作符和我上篇文章總結的Rx ...
  • 在Startup ConfigureServices 註冊本地化所需要的服務AddLocalization和 Configure<RequestLocalizationOptions> public void ConfigureServices(IServiceCollection services ...
  • 為什麼需要持久化,以及Redis持久化的RDB方式在這篇文章講的已經很透徹了,足以弔打面試官了。而且此篇內容需要RDB文章的內容支持,所以建議先看下:看完這篇還不懂Redis的RDB持久化,你們來打我! 一、什麼是AOF 它也是Redis持久化的重要手段之一,aof->Append Only Fil ...
  • 先上圖: @IT程式猿 微博網友評論: @迢書:前同事的,親眼見過 @AvenGeeker:Bug 404 @科技州:這是要逼死強迫症 @小島一瞥:哈哈哈哈哈我老家的車 最後小編整理了一套技術資料不僅能精準消除技術盲點、累計面試經驗,更可以攻剋JVM、Spring、分散式、微服務等技術難題。 海量電 ...
  • 概括來說,分三步: 1,首先找到是哪個進程的CPU占有率飆到了100%。 2,根據進程號pid,定位到是哪個線程,找到對應線程的tid。 3,導出對應線程的dump日誌文件,分析日誌文件定位具體代碼。 要解決這個問題,你應該具備以下技能: 1,linux的top命令。 2,jvm監控工具jps。 3 ...
  • 寫在最後 程式員為何害怕【別人的代碼】呢?這讓我想起一個段子。 寫這段代碼時 只有上帝和我知道他是幹嘛的 現在 只有上帝知道了 別人的代碼,似乎總意味著冗長、晦澀、凌亂,給人一種不想靠近的感覺。搞笑的是,對於一些程式員而言,即使是自己的代碼,在一段時間之後自己再拿來看,也成了【別人的代碼】... 作 ...