如何在 .NET 中优化代码性能

本文关键字:优化 代码 性能 NET | 更新日期: 2023-09-27 17:56:57

我有一个导出作业,将数据从旧数据库迁移到新数据库。 我遇到的问题是用户数量约为 300 万,工作需要很长时间才能完成(15+ 小时)。 我使用的机器只有 1 个处理器,所以我不确定threading是否是我应该做的。 有人可以帮我优化这段代码吗?

static void ExportFromLegacy()
{
    var usersQuery = _oldDb.users.Where(x =>
        x.status == 'active');
    int BatchSize = 1000;
    var errorCount = 0;
    var successCount = 0;
    var batchCount = 0;
    // Using MoreLinq's Batch for sequences
    // https://www.nuget.org/packages/MoreLinq.Source.MoreEnumerable.Batch
    foreach (IEnumerable<users> batch in usersQuery.Batch(BatchSize))
    {
        Console.WriteLine(String.Format("Batch count at {0}", batchCount));
        batchCount++;
        foreach(var user in batch)
        {
            try
            {               
                var userData = _oldDb.userData.Where(x =>
                    x.user_id == user.user_id).ToList();
                if (userData.Count > 0)
                {                   
                    // Insert into table
                    var newData = new newData()
                    {                       
                        UserId = user.user_id; // shortened code for brevity.                       
                    };
                    _db.newUserData.Add(newData);
                    _db.SaveChanges();
                    // Insert item(s) into table
                    foreach (var item in userData.items)
                    {
                        if (!_db.userDataItems.Any(x => x.id == item.id)
                        {
                            var item = new Item()
                            {                               
                                UserId = user.user_id, // shortened code for brevity.   
                                DataId = newData.id // id from object created above
                            };
                            _db.userDataItems.Add(item);                            
                        }
                        _db.SaveChanges();
                        successCount++;
                    }
                }               
            }
            catch(Exception ex)
            {
                errorCount++;
                Console.WriteLine(String.Format("Error saving changes for user_id: {0} at {1}.", user.user_id.ToString(), DateTime.Now));
                Console.WriteLine("Message: " + ex.Message);
                Console.WriteLine("InnerException: " + ex.InnerException);
            }
        }
    }
    Console.WriteLine(String.Format("End at {0}...", DateTime.Now));
    Console.WriteLine(String.Format("Successful imports: {0} | Errors: {1}", successCount, errorCount));
    Console.WriteLine(String.Format("Total running time: {0}", (exportStart - DateTime.Now).ToString(@"hh':mm':ss")));
}

如何在 .NET 中优化代码性能

不幸的是,主要问题是数据库往返的数量。

您进行往返:

  • 对于每个用户,您可以在旧数据库中按用户 ID 检索用户数据
  • 对于每个用户,您将用户数据保存在新数据库中
  • 对于每个用户,您将用户数据项保存在新数据库中

因此,如果你说你有300万用户,每个用户平均有5个用户数据项,这意味着你至少要做3m + 3m + 15m = 2100万数据库往返,这太疯狂了。

显著提高性能的唯一方法是减少数据库往返次数。

批处理 - 按 ID 检索用户

您可以通过一次检索所有用户数据来快速减少数据库往返次数,并且由于您不必跟踪它们,因此使用"AsNoTracking()"可以获得更多性能提升。

var list = batch.Select(x => x.user_id).ToList();
var userDatas = _oldDb.userData
                  .AsNoTracking()
                  .Where(x => list.Contains(x.user_id))
                  .ToList();
foreach(var userData in userDatas)
{
    ....
}

通过此更改,您应该已经节省了几个小时。

批处理 - 保存更改

每次保存用户数据或项目时,都会执行数据库往返。

免责声明:我是项目实体框架扩展的所有者

该库允许执行:

  • 批量保存更改
  • 批量插入
  • 批量更新
  • 批量删除
  • 批量合并

您可以在批处理结束时调用 BulkSaveChanges,也可以创建一个列表来插入并直接使用 BulkInsert,以获得更高的性能。

但是,您必须使用与 newData 实例的关系,而不是直接使用 ID。

foreach (IEnumerable<users> batch in usersQuery.Batch(BatchSize))
{
    // Retrieve all users for the batch at once.
   var list = batch.Select(x => x.user_id).ToList();
   var userDatas = _oldDb.userData
                         .AsNoTracking()
                         .Where(x => list.Contains(x.user_id))
                         .ToList(); 
    // Create list used for BulkInsert      
    var newDatas = new List<newData>();
    var newDataItems = new List<Item();
    foreach(var userData in userDatas)
    {
        // newDatas.Add(newData);
        // newDataItem.OwnerData = newData;
        // newDataItems.Add(newDataItem);
    }
    _db.BulkInsert(newDatas);
    _db.BulkInsert(newDataItems);
}

编辑:回答子问题

newDataItem 的属性之一是 newData 的 ID。(例如。 newDataItem.newDataId.) 所以 newData 必须先保存在 以生成其 ID。如果有一个 另一个对象的依赖关系?

必须改用导航属性。通过使用导航属性,您将永远不必指定父 ID,而是设置父对象实例。

public class UserData
{
    public int UserDataID { get; set; }
    // ... properties ...
    public List<UserDataItem> Items { get; set; }
}
public class UserDataItem
{
    public int UserDataItemID { get; set; }
    // ... properties ...
    public UserData OwnerData { get; set; }
}
var userData = new UserData();
var userDataItem = new UserDataItem();
// Use navigation property to set the parent.
userDataItem.OwnerData = userData;

教程:配置一对多关系

另外,我在示例代码中没有看到批量保存更改。会不会 必须在所有批量插入之后调用?

大容量插入直接插入到数据库中。您不必指定"SaveChanges"或"BulkSaveChanges",一旦调用该方法,它就完成了;)

下面是使用 BulkSaveChanges 的示例:

foreach (IEnumerable<users> batch in usersQuery.Batch(BatchSize))
{
    // Retrieve all users for the batch at once.
   var list = batch.Select(x => x.user_id).ToList();
   var userDatas = _oldDb.userData
                         .AsNoTracking()
                         .Where(x => list.Contains(x.user_id))
                         .ToList(); 
    // Create list used for BulkInsert      
    var newDatas = new List<newData>();
    var newDataItems = new List<Item();
    foreach(var userData in userDatas)
    {
        // newDatas.Add(newData);
        // newDataItem.OwnerData = newData;
        // newDataItems.Add(newDataItem);
    }
    var context = new UserContext();
    context.userDatas.AddRange(newDatas);
    context.userDataItems.AddRange(newDataItems);
    context.BulkSaveChanges();
}

BulkSaveChanges 比 BulkInsert 慢,因为必须使用实体框架中的一些内部方法,但仍然比 SaveChanges 快得多。

在此示例中,我为每个批处理创建一个新上下文,以避免内存问题并获得一些性能。如果对所有批次重复使用相同的上下文,则 ChangeTracker 中将有数百万个跟踪的实体,这绝不是一个好主意。

实体框架对于导入大量数据来说是一个非常糟糕的选择。 我从个人经验中知道这一点。

话虽如此,当我尝试以与您相同的方式使用它时,我发现了一些优化方法。

Context将在您添加对象时缓存对象,并且您执行的插入次数越多,将来的插入速度就越慢。 我的解决方案是在处理该实例并创建一个新实例之前将每个上下文限制为大约 500 个插入。 这显著提高了性能。

我能够利用多个线程来提高性能,但您必须非常小心资源争用。 每个线程肯定都需要自己的Context,甚至不要考虑尝试在线程之间共享它。 我的机器有 8 个内核,所以线程可能不会对你有多大帮助;只有一个核心,我怀疑它根本无法帮助您。

使用 AutoDetectChangesEnabled = false; 关闭更改跟踪,更改跟踪非常慢。 不幸的是,这意味着您必须修改代码才能直接通过上下文进行所有更改。 不再Entity.Property = "Some Value";,它变得Context.Entity(e=> e.Property).SetValue("Some Value");(或类似的东西,我不记得确切的语法),这使得代码变得丑陋。

您执行的任何查询都绝对应该使用 AsNoTracking

有了所有这些,我能够将 ~20 小时的过程缩短到大约 6 小时,但我仍然不建议为此使用 EF。 这是一个非常痛苦的项目,几乎完全是由于我对 EF 添加数据的选择不佳。 请使用其他东西...别的东西。。。

我不想给人的印象是 EF 是一个糟糕的数据访问库,它非常适合它的设计目的,不幸的是这不是它的设计目的。

我可以考虑几个选项。

1)可以通过移动_db来稍微提高速度。将 Foreach() 下的 SaveChanges() 放在 foreach() 右括号下

foreach (...){
}
successCount += _db.SaveChanges();

2) 将项目添加到列表,然后添加到上下文中

List<ObjClass> list = new List<ObjClass>();
foreach (...)
{
  list.Add(new ObjClass() { ... });
}
_db.newUserData.AddRange(list);
successCount += _db.SaveChanges();

3)如果是大量的达达,节省一束

List<ObjClass> list = new List<ObjClass>();
int cnt=0;
foreach (...)
{
  list.Add(new ObjClass() { ... });
  if (++cnt % 100 == 0) // bunches of 100
  {
    _db.newUserData.AddRange(list);
    successCount += _db.SaveChanges();
    list.Clear();
    // Optional if a HUGE amount of data
    if (cnt % 1000 == 0)
    {
      _db = new MyDbContext();
    } 
  }
}
// Don't forget that!
_db.newUserData.AddRange(list);
successCount += _db.SaveChanges();
list.Clear();

4) 如果 TOOOO 很大,请考虑使用散装插入。互联网上有一些例子,周围有一些免费的图书馆。参考: https://blogs.msdn.microsoft.com/nikhilsi/2008/06/11/bulk-insert-into-sql-from-c-app/

在大多数这些选项中,您都失去了对错误处理的一些控制,因为很难知道哪个选项失败了。