记一次事务超时

起因

最近遇到线上一个问题,无法保存成功,记录的错误日志,是事务超时(事务超时时间为600秒).这个功能逻辑其实比较复杂的,一直想优化(还没来的急,早期数据没那么多),查询了很多不同的数据,放在内存中,让后进行各种计算.最终在把结果保存到数据库中.因为这一块整体都是使用EF处理的.
  1. 查询各种数据,放入List中(这个数据有几十万条)
  2. 进行各种计算,修改数据(这个也要几十万条)
  3. 计算后的结果保存到数据库中(插入的数据有两三万条)

2和3在方法执行完成后提交事务,都是逐条修改的,排除原因后,发现主要有2个地方比较耗时.

1. FirstOrDefault竟然是瓶颈

//模拟实体
public class Person
{
    public string Code { get; set; }

    public string Name { get; set; } 
}

//模拟代码
private void  TestData()
{
    List<Person> list1 = new List<Person>(); //50万条数据

    List<Person> list2 = new List<Person>(); //1万条数据
            
    for (int i = 0;i< list1.Count;i++)
    {
        var item = list1[i];
        var val = list2.FirstOrDefault(p => p.Code == item.Code && 
                                            p.Name == item.Name); //FirstOrDefault这里很耗时    } 
}

当集合数据比较少的时候,使用FirstOrDefault查找是没有问题的,FirstOrDefault内部也是循坏遍历的,最好的情况,是第一个匹配成功,最坏的情况,就是循坏查找了一万次.我们看一下FirstOrDefault源码:

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source)
    {
        if (predicate(element)) return element;
    }
    return default(TSource);
}

解决方法,就是修改list2由List为Dictionary类型.

private void TestData()
{
    List<Person> list1 = new List<Person>(); //50万条数据
    //改为Dictionary,设置初始化大小和不区分key的大小写
    Dictionary<string, Person> maps = new Dictionary<string, Person>(100000, StringComparer.OrdinalIgnoreCase);

    for (int i = 0; i < list1.Count; i++)
    {
        var item = list1[i];
        var key = $"{item.Code}{item.Name}";
        if (maps.TryGetValue(key, out Person person))
        {
            //处理计算逻辑
        }
    }
}

修改之后,这一块处理的速度,不再是瓶颈,还有两个问题,修改数据改成批量更新,每次执行2000条数据.另外一个就是批量插入

2. 使用SqlBulkCopy批量插入

private async Task BatchInsert(SqlConnection con, DataTable dataTable, string tableName)
{
    using (var bulkCopy = new SqlBulkCopy(con))
    {
        bulkCopy.BatchSize = 2000;  //每次执行2000条插入
        bulkCopy.DestinationTableName = tableName;

        //将DataTable的列名映射到SqlBulkCopy列
        foreach (DataColumn colInfo in dataTable.Columns)
        {
            bulkCopy.ColumnMappings.Add(colInfo.ColumnName, colInfo.ColumnName);
        }

        //异步写入到数据库中
        await bulkCopy.WriteToServerAsync(dataTable);
    }
}

结果

通过这几种方式如减少查找时间和批量修改数据,及批量插入.这一块功能在一段时间不会成为瓶颈.当工作中遇到性能瓶颈的时候,不能靠主观的猜测,需要用工具和数据分析,让后针对性的去优化.
秋风 2023-03-19