记一次使用ConcurrentDictionary优化程序性能的经验总结
项目情形
最近做项目发现有个业务逻辑性能效率巨慢, 实际上是扫描cosmos上面16个文件夹下面的数据, 每个folder下面大概分为100来个对应user的fodler, 然后对应user folder下面存放的是user的数据. 原逻辑是一个folder一个folder去scan, 然后将统计的数据按照 user和size存放到一个dictionary中, 最后汇总统计并且发邮件. 其中影响效率的部分有当前运行环境与cosmos的交互上, 不同的环境快慢不同. 另外一个就是code逻辑是串行的. 这样会导致效率很差. 整体运行完一遍, 在慢的环境上要48小时, 快的环境也得将近20小时. 于是我开始想优化code逻辑. 使用并行方式尝试提升运行效率.
使用ConcurrentDictionary
我之前大概了解过一些关于ConcurrentDictionary的概念, 知道它是一个线程安全的字典缓存. 可以用于并发场景. 对于我目前来说作为解决方案应该是适配的. 我准备使用一个核心ConcurrentDictionary作为数据缓存, 然后启用16个Task去扫描cosmos上面的16个root folder. 每个task将扫描得到的数据记录至ConcurrentDictionary当中. 最后当所有的task运行完毕后, 将ConcurrentDictionary中的值完善一下发出邮件. 初步做了一下, 在一个快的环境上, 运行了2个小时, 程序完成业务逻辑. 也就是说效率从原来的20小时提升至2小时, 提升了10倍. 效果还是非常显著的. 下面我来介绍一下具体的实现和深入探究一下ConcurrentDictionary的底层实现.
核心缓存只一行代码:
public static ConcurrentDictionary<string, UserCosmosInfo> aggregate = new ConcurrentDictionary<string, UserCosmosInfo>();
其中string为user的名字, 不会重复, 作为key正好. 然后UserCosmosInfo
是我封装的一个类, 类里面的属性是user后面需要用的:
public class UserCosmosInfo
{
public string Name { get; set; }
public long TotalStorage { get; set; }
public long Last1WeekAddedStorage { get; set; }
public long Last2WeekAddedStorage { get; set; }
public long Last1MonthAddedStorage { get; set; }
public long Last6MonthsAddedStorage { get; set; }
public long OtherMoreThan6Months { get; set; }
public List<CosmosStreamInfo> TopBigFiles { get; set; }
}
启动16个Task分别扫描数据:
public static void AggregateCosmosFolder(string baseFolder, string[] filter, bool recursive = true)
{
var folderCount = 16;
var tasks = new Task[folderCount];
for (int i = 0; i < folderCount; i++)
{
int param = i;
tasks[i] = Task.Run(() => AggregateCosmosFolderInTask(param, baseFolder, filter, true));
}
Task.WaitAll(tasks);
}
实际每个task运行的业务类(简略版):
private static void AggregateCosmosFolderInTask(int currentFolder, string baseFolder, string[] filter, bool recursive = true)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
for (int j = 0; j < filter.Length; j++)
{
string u = filter[j];
string prefix = $"{baseFolder}/_{currentFolder.ToString("x")}/{u}/";
if (CosmosDirExist(prefix))
{
IEnumerable<StreamInfo> streams = GetFiles(prefix, recursive);
if (streams != null)
{
foreach (StreamInfo s in streams)
{
//调用 tryAdd, 将key u 和 value 加入缓存, 此处 tryAdd的底层实现了并发控制, 稍后我们看看底层的代码实现....
Program.aggregate.TryAdd(u, new UserCosmosInfo()
{
///省略代码篇幅
});
// new 一个新的 value, 这个value是需要更新到缓存中的.
var newValue = new UserCosmosInfo()
{
///省略代码篇幅
};
// AddOrUpdate 方法用于更新缓存的数据, 底层同样是并发控制做的很到位了, 所以我们直接放心使用......
// 注意它的参数, key 传 u, 将上面的 newValue 传进去, 在写一个 Fun<> 委托, 委托在执行时也是线程安全的, 至于实现方式也需要看底层源码.
Program.aggregate.AddOrUpdate(u, newValue, (key, existingValue) =>
{
existingValue.TotalStorage += newValue.TotalStorage;
return existingValue;
});
}
}
}
}
}
新的业务类的实现大致是这样得. 主要是使用了 ConcurrentDictionary 的一些API, 下一步我将探究底层的实现
今天没时间了, 下次继续写.
参考链接
https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/how-to-add-and-remove-items
https://stackoverflow.com/questions/30225476/task-run-with-parameters