[NewLife.XCode]實體隊列(多線程生產的大數據集中保存)

NewLife.XCode是一個有15年曆史的開源數據中間件,支持netcore/net45/net40,由新生命團隊(2002~2020)開發完成並維護至今,以下簡稱XCode。

整個系列教程會大量結合示例代碼和運行日誌來進行深入分析,蘊含多年開發經驗於其中,代表作有百億級大數據實時計算項目。

開源地址:https://github.com/NewLifeX/X (求star, 1067+)

 

在大數據分析處理中,需要對海量數據進行添刪改操作,常規單行操作難以滿足要求,批量操作勢在必行!

飛仙(http://feixian.newlifex.com/)有收藏各種數據庫批量插入數據的性能排行榜,其中MySql冠軍是60萬tps,SQLite冠軍是56.6萬tps

然而很多時候,數據來自多個渠道(多線程、多網絡連接),單個渠道數據量不大,甚至只有一行,就難以使用批量添刪改操作了。例如物聯網數據採集、埋點日誌等,在多線程上有大量數據需要寫入。因此,XCode創造性設計了實體隊列技術

!!閱讀本文之前,建議閱讀https://www.yuque.com/smartstone/xcode/batch

什麼是實體隊列

要說實體隊列EntityDeferredQueue,就不得不提它的基類延遲隊列DeferredQueue。

延遲隊列DeferredQueue的核心思想就是“湊批”,把要處理的零散數據放入一個“隊列”,然後定時集中處理

例如物聯網採集服務端從多個連接收到數據,需要寫入數據庫,爲了提升吞吐,可以把實體數據放入延遲隊列,然後定時的落庫,此時,延遲隊列得到一批數據,可以使用批量插入技術。

實際上DeferredQueue內部並不是一個隊列,而是一個併發字典,因爲有些業務場景,需要在“入隊列”時去重,例如統計數據,需要拿出某省份的統計數據,多次累加後集中保存。

private static readonly DeferredQueue _statCache = new EntityDeferredQueue { Name = "Gun", Action = EntityActions.Save };
private static void SaveStat(DateTime date, Int32 provinceID, String kind, ScanKinds scanKind, String code)
{
    var key = $"{date:yyMMdd}_{provinceID}_{kind}";
    var stat = _statCache.GetOrAdd(key, k => GunProvinceStat.FindByKey(k, true) ?? new GunProvinceStat());

    stat.StatDate = date;
    stat.Kind = kind;
    stat.ProvinceID = provinceID;
    stat.LastCode = code;

    stat.ProcessStat(scanKind);

    _statCache.Commit(key);
}

主要流程


對於統計型數據來說,可以在內存裏面多次累加計算指標,然後一次性保存,並且是批量保存,極大減少了數據庫寫入次數。這是大數據分析必備利器!

延遲隊列主要屬性

/// <summary>跟蹤數。達到該值時輸出跟蹤日誌,默認1000</summary>
public Int32 TraceCount { get; set; } = 1000;

/// <summary>週期。默認10_000毫秒</summary>
public Int32 Period { get; set; } = 10_000;

/// <summary>最大個數。超過該個數時,進入隊列將產生堵塞。默認100_000</summary>
public Int32 MaxEntity { get; set; } = 100_000;

/// <summary>批大小。默認5_000</summary>
public Int32 BatchSize { get; set; } = 5_000;

/// <summary>等待借出對象確認修改的時間,默認3000ms</summary>
public Int32 WaitForBusy { get; set; } = 3_000;

/// <summary>保存速度,每秒保存多少個實體</summary>
public Int32 Speed { get; private set; }

/// <summary>是否異步處理。默認true表示異步處理,共用DQ定時調度;false表示同步處理,獨立線程</summary>
public Boolean Async { get; set; } = true;

回過頭來,實體隊列EntityDeferredQueue作爲延遲隊列的擴展延伸,實際上是定義了“隊列數據”的處理行爲。延遲隊列只負責收集數據和定時調度,實際處理行爲Process需要擴展。 

EntityDeferredQueue定義了 Save/Insert/Update/Upsert/Delete 等行爲供選擇。

如何使用實體隊列提升吞吐

再次深入分析前文的例子

private static readonly DeferredQueue _statCache = new EntityDeferredQueue { Name = "Gun", Action = EntityActions.Save };
private static void SaveStat(DateTime date, Int32 provinceID, String kind, ScanKinds scanKind, String code)
{
    var key = $"{date:yyMMdd}_{provinceID}_{kind}";
    var stat = _statCache.GetOrAdd(key, k => GunProvinceStat.FindByKey(k, true) ?? new GunProvinceStat());

    stat.StatDate = date;
    stat.Kind = kind;
    stat.ProvinceID = provinceID;
    stat.LastCode = code;

    stat.ProcessStat(scanKind);

    _statCache.Commit(key);
}

這是一個非常簡單的數據分析項目,統計每天各省每一種掃描類型的操作次數。日均分析處理5億行數據,每一行數據都要識別出日期、省份、類別等字段,也就是SaveStat每天要調用5億次,結果數據分類存入統計表。共31省份27種類別,每日統計行數約800行(並非每個省都有全部類別)。通俗來講,5億行數據,分組聚合得到800行,實時計算,每5秒計算一次。

採用流式計算框架,逐行遍歷5億行實時數據,如果Insert/Update數據庫5億次,顯然很不現實!

平均每行寫入62.5萬次(5億/800),如果能夠在內存裏面“湊一湊”,每1000次更新,才寫入一次數據庫,那麼總寫入次數降低爲50萬次,平均每行寫入625次。

實體隊列/延遲隊列,正是爲了這類場景而設計!

首先,根據業務去構造一個唯一key,在這裏就是日期+省份+類別;

其次,GetOrAdd嘗試從隊列裏獲取該key對應的統計對象,99%時候內存命中,如果不存在,則查數據庫或者new一個;

再次,取得統計對象後,可以進行字段累加,stat.ProcessStat(scanKind);

最後,Commit告訴隊列,該key對應的實體對象已經使用完成,可以提交;

在延遲隊列內部,定時(Period=10_000ms)執行一次保存,把內存裏面的統計對象批量保存到數據庫,並清空隊列。

這裏遇到的第一個問題就是,少量統計對象仍然使用怎麼辦?請放心,定時任務會等待一定時間(WaitForBusy=3000ms),如果使用方Commit則提前完成。因此,上面的Commit可以不要,效果會變差一些,同時,統計邏輯必須儘快完成(<3000ms)。

第二個問題很重要,定時間隔(Period=10_000ms)之內,內存數據是高危狀態,如果此時進程退出,則意味着統計數據丟失。標準架構應該是在數據落庫以後做Ack確認,但是原始數據實在太多(5億),很不現實。因此,實際工作中,我們是通過提升系統可靠性來規避該問題,採用螞蟻調度AntJob,結合分佈式多節點部署,在實時計算中,內存保留數據並不多。每次需要更新程序時,先停止調度一分鐘,等待數據落庫和冷卻,才能推出應用進程。在數據分析領域,一般允許有一定的數據誤差(<0.01%),或者白天實時計算加夜晚離線重算的模式!

實際經驗表明,只要應用沒有非法退出,不存在數據丟失問題!

再來看看 ProcessStat內部,(這裏的GunProvinceStat是XCode實體類,一張統計表)

public void ProcessStat(ScanKinds kind)
{
    //stat.Total++;
    Interlocked.Increment(ref _Total);

    switch (kind)
    {
        case ScanKinds.Receipt:
            //stat.Receipts++;
            Interlocked.Increment(ref _Receipts);
            break;
        case ScanKinds.SendBill:
        case ScanKinds.SendAir:
            //stat.Sends++;
            Interlocked.Increment(ref _Sends);
            break;
        case ScanKinds.SendBag:
            Interlocked.Increment(ref _SendBags);
            break;
        case ScanKinds.ComeBill:
        case ScanKinds.ComeAir:
            //stat.Comes++;
            Interlocked.Increment(ref _Comes);
            break;
        case ScanKinds.ComeBag:
            Interlocked.Increment(ref _ComeBags);
            break;
        case ScanKinds.SendCar:
        case ScanKinds.ComeCar:
            Interlocked.Increment(ref _Cars);
            break;
        case ScanKinds.Dispatch:
            //stat.Dispatchs++;
            Interlocked.Increment(ref _Dispatchs);
            break;
        case ScanKinds.Sign:
            //stat.Signs++;
            Interlocked.Increment(ref _Signs);
            break;
        case ScanKinds.Back:
            Interlocked.Increment(ref _Backs);
            break;
        case ScanKinds.Problem:
            Interlocked.Increment(ref _Problems);
            break;
        case ScanKinds.Stay:
        case ScanKinds.Other:
        case ScanKinds.Input:
        case ScanKinds.Order:
        case ScanKinds.Electronic:
        default:
            Interlocked.Increment(ref _Others);
            break;
    }
}

數據表結構

<Table Name="GunProvinceStat" Description="巴槍省份統計" IgnoreNameCase="False">
  <Columns>
    <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
    <Column Name="StatDate" DataType="DateTime" Description="統計日期" />
    <Column Name="ProvinceID" DataType="Int32" Description="省份。0表示全國" />
    <Column Name="Kind" DataType="String" Description="類別。All表示所有類型" />
    <Column Name="Total" DataType="Int64" Description="總次數" />
    <Column Name="Receipts" DataType="Int64" Description="收件數" />
    <Column Name="Sends" DataType="Int64" Description="發件數" />
    <Column Name="Comes" DataType="Int64" Description="到件數" />
    <Column Name="Dispatchs" DataType="Int64" Description="派件數" />
    <Column Name="Signs" DataType="Int64" Description="簽收數" />
    <Column Name="SendBags" DataType="Int64" Description="發包數" />
    <Column Name="ComeBags" DataType="Int64" Description="到包數" />
    <Column Name="Cars" DataType="Int64" Description="掃車數" />
    <Column Name="Backs" DataType="Int64" Description="退件數" />
    <Column Name="Problems" DataType="Int64" Description="問題件數" />
    <Column Name="Others" DataType="Int64" Description="其它數" />
    <Column Name="LastCode" DataType="String" Description="最後單號" />
    <Column Name="CreateTime" DataType="DateTime" Description="創建時間" />
    <Column Name="UpdateTime" DataType="DateTime" Description="更新時間" />
  </Columns>
  <Indexes>
    <Index Columns="StatDate,ProvinceID,Kind" Unique="True" />
    <Index Columns="Kind,ProvinceID" />
  </Indexes>
</Table>

 

 

系列教程

NewLife.XCode教程系列[2019版]

  1. 增刪改查入門。快速展現用法,代碼配置連接字符串
  2. 數據模型文件。建立表格字段和索引,名字以及數據類型規範,推薦字段(時間,用戶,IP)
  3. 實體類詳解。數據類業務類,泛型基類,接口
  4. 功能設置。連接字符串,調試開關,SQL日誌,慢日誌,參數化,執行超時。代碼與配置文件設置,連接字符串局部設置
  5. 反向工程。自動建立數據庫數據表
  6. 數據初始化。InitData寫入初始化數據
  7. 高級增刪改。重載攔截,自增字段,Valid驗證,實體模型(時間,用戶,IP)
  8. 髒數據。如何產生,怎麼利用
  9. 增量累加。高併發統計
  10. 事務處理。單表和多表,不同連接,多種寫法
  11. 擴展屬性。多表關聯,Map映射
  12. 高級查詢。複雜條件,分頁,自定義擴展FieldItem,查總記錄數,查彙總統計
  13. 數據層緩存。Sql緩存,更新機制
  14. 實體緩存。全表整理緩存,更新機制
  15. 對象緩存。字典緩存,適用用戶等數據較多場景。
  16. 百億級性能。字段精煉,索引完備,合理查詢,充分利用緩存
  17. 實體工廠。元數據,通用處理程序
  18. 角色權限。Membership
  19. 導入導出。Xml,Json,二進制,網絡或文件
  20. 分表分庫。常見拆分邏輯
  21. 高級統計。聚合統計,分組統計
  22. 批量寫入。批量插入,批量Upsert,異步保存
  23. 實體隊列。寫入級緩存,提升性能。
  24. 備份同步。備份數據,恢復數據,同步數據
  25. 數據服務。提供RPC接口服務,遠程執行查詢,例如SQLite網絡版
  26. 大數據分析。ETL抽取,調度計算處理,結果持久化 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章