使用并行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
使用并行Foreach优化SqlSugarMapper的更多相关文章
- NET中并行开发优化
NET中并行开发优化 让我们考虑一个简单的编程挑战:对大数组中的所有元素求和.现在可以通过使用并行性来轻松优化这一点,特别是对于具有数千或数百万个元素的巨大阵列,还有理由认为,并行处理时间应该与常规时 ...
- 关于 SSIS 并行foreach loop的一个设计思路
SSIS 包在控制流方面的性能优化,主要是提高并行度. 可以设置并发线程数MaxConcurrentExecuteables. SSIS中的foreach loop container 不是并行执行任 ...
- 【58沈剑架构系列】mysql并行复制优化思路
一.缘起 mysql主从复制,读写分离是互联网用的非常多的mysql架构,主从复制最令人诟病的地方就是,在数据量较大并发量较大的场景下,主从延时会比较严重. 为什么mysql主从延时这么大? 回答:从 ...
- .Net中的并行编程-6.常用优化策略
本文是.Net中的并行编程第六篇,今天就介绍一些我在实际项目中的一些常用优化策略. 一.避免线程之间共享数据 避免线程之间共享数据主要是因为锁的问题,无论什么粒度的锁 ...
- Parallel.ForEach() 并行循环
现在的电脑几乎都是多核的,但在软件中并还没有跟上这个节奏,大多数软件还是采用传统的方式,并没有很好的发挥多核的优势. 微软的并行运算平台(Microsoft’s Parallel Computing ...
- Java 进阶7 并发优化 1 并行程序的设计模式
本章重点介绍的是基于 Java并行程序开发以及优化的方法,对于多核的 CPU,传统的串行程序已经很好的发回了 CPU性能,此时如果想进一步提高程序的性能,就应该使用多线程并行的方式挖掘 CPU的 ...
- java-11-Stream优化并行流
并行流 多线程 把一个内容分成多个数据块 不同线程分别处理每个数据块的流 串行流 单线程 一个线程处理所有数据 java8 对并行流优化 StreamAPI 通过pa ...
- [源码解析] PyTorch分布式优化器(2)----数据并行优化器
[源码解析] PyTorch分布式优化器(2)----数据并行优化器 目录 [源码解析] PyTorch分布式优化器(2)----数据并行优化器 0x00 摘要 0x01 前文回顾 0x02 DP 之 ...
- [源码解析] PyTorch分布式优化器(3)---- 模型并行
[源码解析] PyTorch分布式优化器(3)---- 模型并行 目录 [源码解析] PyTorch分布式优化器(3)---- 模型并行 0x00 摘要 0x01 前文回顾 0x02 单机模型 2.1 ...
随机推荐
- Java文件操作(求各专业第一名的学生)
两个文件:info.txt 存放学生基本信息 学号 学院 专业 姓名 1001 计算机学院 软件工程 刘月 1002 生物工程 服装设计 孙丽 score.txt存放分数信息 学号 学科 成绩 100 ...
- ORACLE CACHE BUFFER CHAINS原理
原理图如下: 一个cache buffer chains 管理多个hash bucket,受隐含参数:_db_block_hash_buckets(控制管理几个hash bucket)
- mysql触发器实例说明
触发器是一类特殊的事务 ,可以监视某种数据操作(insert/update/delete),并触发相关操作(insert/update/delete). 看以下事件: 完成下单与减少库存的逻辑 Ins ...
- 【编程思想】【设计模式】【其他模式】graph_search
Python版 https://github.com/faif/python-patterns/blob/master/other/graph_search.py #!/usr/bin/env pyt ...
- virtualBox 系统移植
把virtualbox已经存在的系统移植到其他机器. 1.把系统如下文件考到一个安装了virtualbox的机器. 2.点击控制-->注册 然后浏览到复制的文件路径. 3.修改uuid 不管是l ...
- Can references refer to invalid location in C++?
在C++中,引用比指针更加的安全,一方面是因为引用咋定义时必须进行初始化,另一方面是引用一旦被初始化就无法使其与其他对象相关联. 但是,在使用引用的地方仍然会有一些例外. (1)Reference t ...
- 【Java】【IDE】【Jetbrain Idea】Intellij IDEA 快捷键整理
[常规] Ctrl+Shift + Enter,语句完成 "!",否定完成,输入表达式时按 "!"键 Ctrl+E,最近的文件 Ctrl+Shift+E,最近更 ...
- list.jsp页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %><%@tag ...
- C++STL标准库学习笔记(四)multiset续
自定义排序规则的multiset用法 前言: 在这个笔记中,我把大多数代码都加了注释,我的一些想法和注解用蓝色字体标记了出来,重点和需要关注的地方用红色字体标记了出来,只不过这一次的笔记主要是我的补充 ...
- redis迁移工具redis-migrate-tool
目录 一.简介 二.测试 三.安装 四.验证 一.简介 redis-migrate-tool是在redis之间迁移数据的一个方便且有用的工具.他会已服务方式不断同步两边的数据.等到合适时间,中断red ...