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版]
- 增刪改查入門。快速展現用法,代碼配置連接字符串
- 數據模型文件。建立表格字段和索引,名字以及數據類型規範,推薦字段(時間,用戶,IP)
- 實體類詳解。數據類業務類,泛型基類,接口
- 功能設置。連接字符串,調試開關,SQL日誌,慢日誌,參數化,執行超時。代碼與配置文件設置,連接字符串局部設置
- 反向工程。自動建立數據庫數據表
- 數據初始化。InitData寫入初始化數據
- 高級增刪改。重載攔截,自增字段,Valid驗證,實體模型(時間,用戶,IP)
- 髒數據。如何產生,怎麼利用
- 增量累加。高併發統計
- 事務處理。單表和多表,不同連接,多種寫法
- 擴展屬性。多表關聯,Map映射
- 高級查詢。複雜條件,分頁,自定義擴展FieldItem,查總記錄數,查彙總統計
- 數據層緩存。Sql緩存,更新機制
- 實體緩存。全表整理緩存,更新機制
- 對象緩存。字典緩存,適用用戶等數據較多場景。
- 百億級性能。字段精煉,索引完備,合理查詢,充分利用緩存
- 實體工廠。元數據,通用處理程序
- 角色權限。Membership
- 導入導出。Xml,Json,二進制,網絡或文件
- 分表分庫。常見拆分邏輯
- 高級統計。聚合統計,分組統計
- 批量寫入。批量插入,批量Upsert,異步保存
- 實體隊列。寫入級緩存,提升性能。
- 備份同步。備份數據,恢復數據,同步數據
- 數據服務。提供RPC接口服務,遠程執行查詢,例如SQLite網絡版
- 大數據分析。ETL抽取,調度計算處理,結果持久化