互聯網那些事之數據丟失

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

互聯網那些事之數據丟失 本系列故事的所有案例和解決方案只是筆者以前在互聯網工作期間的一些事例,僅供大家參考,實際操作應該根據業務和項目情況設計,歡迎大家留言提出寶貴的意見 背景 小王和小明分別維護分散式系統中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服務按照老梁的思路,重新調整了代碼,異常處理流程如下:

 


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

-Advertisement-
Play Games
更多相關文章
  • "正則表達式知識" 利用模式匹配,幾行代碼搞定 ...
  • 講究一個對稱性 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 ...
  • 第一種 第二種 第三種 第四種 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...