使用Parallel.Foreach優化SqlSugarMapper


最近在遷移公司導入導出項目時,發現導出速度特別慢,大概2K數據需要導出近半個小時,通過在程序各個地方埋點,最終定位到了Sqlsugar的Mapper中,隨后通過並行Foreach單獨抽出Mapper中的業務方法,性能提升近30倍,當然,此屬於個人總結可能並不適用於讀者業務邏輯,最重要的一點:業務上優化遠比技術層面優化要來得快,效率更高!

有性能瓶頸嗎?

SqlSugar的Mapper經過打印日志發現,即使mapper中的執行是串行的,在內存中處理數據速度也是非常快的,但是當在mapper中有些耗時操作時,數據量越大處理時間便成線性增長。
例如在此導出業務中,有涉及到手機號碼加解密的邏輯,因為解密耗時將近0.5秒,所以導出1000條數據的時候,光手機號碼處理就需要耗時500秒,此間還沒法做其他操作,所以我認定性能瓶頸在Sqlsugar的Mapper上,准備從此處開刀。

定位問題所在

秉着大膽猜想,小心求證的原則,

既然猜想問題是處在Mapper上,先上代碼。

這是一個非常復雜的數據查詢,從行號即可看到,此方法有近600行代碼,給他稍微整理一下,這個查詢結構如下:

var aList = await DbContext.Queryable<TableA>().Where(x => x.code == input.Acode).ToList();
var bList = ...
var cList = ...
......

var queryable = DBContext.Queryable<TableMaster>().Where(x => x.SystemId == input.SystemId)
          .WhereIF(!input.Phone.IsNullOrWhiteSpace(), x => x.Phone == input.Phone.Trim().Encrypt())
          .WhereIF(input.Acode>0),x => aList.Contains(x.Acode))
          .WhereIF(input.Bcode>0),x => bList.Contains(x.Bcode))
......
          .OrderBy(x => x.CreateTime, OrderByType.Desc)
          .Select(x => new RetrunListModel
                {
                    ID = x.Id,
                    SystemId = x.SystemId,
                    Phone = x.Phone,
                    Acode = x.Acode,
                    Bcode = x.Bcode,
                    ......
                })
          .Mapper((model, cache) =>
              {
               // 類型一:查詢中間表賦值
               // 使用Master表查詢結果中的ACode,從A表中的Name查詢數來
                var aList = cache.Get(h =>
                {
                  var aCodeList = h.Where(x => x.ACode > 0).Select(x => x.ACode).Distinct().ToList();
                  return DbContext.Queryable<TableA>().Where(x => x.SystemId == input.SystemId)
                               .Where(x => aCodeList.Contains(x.ACode))
                               .Select(x => new TableA
                               {
                                   Id = x.Id,
                                   Name = x.Name,
                               }).ToList();
                }
                // 將查詢的結果賦值給model,即最終的接收結果
                model.AName = aCodeList.Where(x => x.Id == n.ACode).FirstOrDefault()?.Name ?? "";
                
                // 類型二:對數據解密
                if(input.IsDecrypt)
                {
                    model.Phone = xxxService.DecryptPhone(model.Phone)
                }                
                ......
              });

直接看向Mapper,在這里的業務有兩種類型:

  • 第一種類型,通過從表去對查詢結果中的字段賦值,這里只會在第一次查詢從表有耗時操作,因為他會在查詢子表后,將結果存在緩存中,即aList,后續取值都從緩存中取,故后續基本無需耗時
  • 第二種類型,對查詢結果的某一字段進行處理,例如加解密,這里調用的是解密方法,故每次都是需要將該字段傳遞至解密服務中,因此每次都需耗時去解密,所以性能瓶頸卡在這里。

干說可能不好懂,畫了張圖

並行Foreach

很顯然從上圖可以看出,由於循環解密需要耗時較長,就算把Mapper單獨抽出來,還是需要循環去將字段解密,看起來無解,但是這里可以是使用並行foreach去處理的,也可以用多線程這里不做展開,但是再次給讀者提個醒,業務上去做優化遠比技術上優化來的快,效率更高!

認識並行庫

.Net Framework4 引入了新的Task Parallel Library(任務並行庫,TPL),它支持數據並行、任務並行和流水線。
當並行循環運行時,TPL會將數據源按照內置的分區算法(或者你可以自定義一個分區算法)將數據划分為多個不相交的子集,然后,從線程池中選擇線程並行地處理這些數據子集,每個線程只負責處理一個數據子集。在后台,任務計划程序將根據系統資源和工作負荷來對任務進行分區。如有可能,計划程序會在工作負荷變得不平衡的情況下在多個線程和處理器之間重新分配工作。
在對任何代碼(包括循環)進行並行化時,一個重要的目標是利用盡可能多的處理器,但不要過度並行化到使行處理的開銷讓任何性能優勢消耗殆盡的程度。比如:對於嵌套循環,只會對外部循環進行並行化,原因是不會在內部循環中執行太多工作。少量工作和不良緩存影響的組合可能會導致嵌套並行循環的性能降低。
由於循環體是並行運行的,迭代范圍的分區是根據可用的邏輯內核數、分區大小以及其他因素動態變化的,因此無法保證迭代的執行順序。
    TPL引入了System.Threading.Tasks ,主類是Task,這個類表示一個異步的並發的操作,然而我們不一定要使用Task類的實例,可以使用Parallel靜態類。
它提供了Parallel.Invoke, Parallel.For,Parallel.Forecah 三個方法
當然此處是我讀了《.net 並發編程實戰》,大神博客以及官方文檔,稍微總結的,后文貼上鏈接,他們文章更詳細。

上代碼

這里的思路就是,先將結果查詢出出來,然后將之前的從表查詢以及字段賦值處理,單獨抽出來通過並行Foreach的方式,快速處理加解密這類耗時操作。

var aList = await DbContext.Queryable<TableA>().Where(x => x.code == input.Acode).ToList();
var bList = ...
var cList = ...
......

var queryable =await DBContext.Queryable<TableMaster>().Where(x => x.SystemId == input.SystemId)
        .WhereIF(!input.Phone.IsNullOrWhiteSpace(), x => x.Phone == input.Phone.Trim().Encrypt())
        .WhereIF(input.Acode>0),x => aList.Contains(x.Acode))
        .WhereIF(input.Bcode>0),x => bList.Contains(x.Bcode))
......
        .OrderBy(x => x.CreateTime, OrderByType.Desc)
        .Select(x => new RetrunListModel
              {
                  ID = x.Id,
                  SystemId = x.SystemId,
                  Phone = x.Phone,
                  Acode = x.Acode,
                  Bcode = x.Bcode,
                  ......
              }).ToListAsync();
              
        var aCodeList = h.Where(x => x.ACode > 0).Select(x => x.ACode).Distinct().ToList();
        var aList = DbContext.Queryable<TableA>().Where(x => x.SystemId == input.SystemId)
                             .Where(x => aCodeList.Contains(x.ACode))
                             .Select(x => new TableA
                             {
                                 Id = x.Id,
                                 Name = x.Name,
                             }).ToList();
        ......
        
          // 並行的方式 給列表綁值
          var rangesize = (int)(clueQueryList.Count / Environment.ProcessorCount) + 1;
          var rangePartitioner = Partitioner.Create(0, clueQueryList.Count, rangesize);
          Parallel.ForEach(rangePartitioner, range =>
          {
              var newList = clueQueryList.Skip(range.Item1).Take(range.Item2 - range.Item1);
              foreach (var model in newList)
              {
                // 字段賦值
                model.AName = aList.Where(x => x.Id == n.ACode).FirstOrDefault()?.Name ?? "";
                // 手機號碼加解密
                model.Phone = xxxService.DecryptPhone(model.Phone)
                ......
              }
          });
              return clueQueryList;
  • 看到並行部分,在將sqlsugar的Mapper抽出來之后,這里使用並行的foreach去處理查詢結果,包括字段賦值,耗時更長的加解密操作。
  • 這里是使用並行foreach與分區器,將需要操作的數據按照邏輯處理器分成指定的塊,然后再並行處理數據,於是可以完全發揮出多核處理器的優勢。


這樣通過數據分塊之后,並行處理,效率會成倍數上升,並且微軟的並行庫(TPL)也針對並行foreach做了許多優化,TPL在幕后使用的負載均衡機制都是非常高效的,比如我們不使用分區器,直接對數據源進行負載均衡的並行執行,這里推薦一個博客,指定最大並行度:https://www.cnblogs.com/QinQouShui/p/12134232.html

System.Threading.Tasks.Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 12 }, range =>
          {
              #region 業務代碼
              #endregion
          });

優化效果


測試用的服務器是八核,前后兩次導出3500條數據,使用SQLSugarMapper與並行Foreach對比速度

總結

大致總結一下幾點

  • 分區器+並行foreach不是銀彈,數據量較大時,他的優勢才能彌補分區所消耗的資源與時間。
  • 邏輯處理越多,多核處理優勢越大
  • 並行處理需要解決多個並行任務處理同一條數據的情況,此文是使用分區器隔離

參考資料

【書籍】《.net並發編程實戰》
【官方文檔】《.NET 中的並行編程》https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/
【博客園】《.Net並行編程高級教程--Parallel》https://www.cnblogs.com/stoneniqiu/p/4857021.html
【博客園】《8天玩轉並發》
https://www.cnblogs.com/huangxincheng/category/368987.html
【博客園】《異步編程:.NET4.X 數據並行》
https://www.cnblogs.com/heyuquan/archive/2013/03/13/parallel-for-foreach-invoke.html
【博客園】《Parallel.ForEach 之 MaxDegreeOfParallelism》
https://www.cnblogs.com/QinQouShui/p/12134232.html
【自己總結】《如何運用並行編程Parallel提升任務執行效率》https://mp.weixin.qq.com/s/3qli3cM9ZLweG9aj-nYdBw


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM