如果不访问代码,将很难回答您的具体问题,但要考虑的主要事项是 EF 生成的数据库查询的大小和复杂性。使用 async/await 将提高 Web 服务器启动请求的响应能力,但负载下的请求处理时间将在很大程度上取决于数据库成为争用点时正在运行的查询。您将希望确保所有查询都尽可能简约。例如,以下 3 种陈述之间存在巨大差异:
var someData = context.SomeTable.Include(x => x.SomeOtherTable)
.ToList()
.Where(x => x.SomeCriteriaMethod())
.ToList();
var someData = context.SomeTable.Include(x => x.SomeOtherTable)
.Where(x => x.SomeField == someField && x.SomeOtherTable.SomeOtherField == someOtherField)
.ToList();
var someData = context.SomeTable
.Where(x => x.SomeField == someField && x.SomeOtherTable.SomeOtherField == someOtherField)
.Select(x => new SomeViewModel
{
SomeTableId = x.SomeTableId,
SomeField = x.SomeField,
SomeOtherField = x.SomeOtherTable.SomeOtherField
}).ToList();
像上面第一个这样的示例效率极低,因为它们最终会在过滤行之前从数据库中加载相关表中的所有数据。尽管您的 Web 服务器可能只传回几行,但它已经从数据库中请求了所有内容。当开发人员面临他们想要过滤 EF 无法转换为 SQL 的值(例如函数)的场景时,这些类型的场景会蔓延到应用程序中,因此他们通过调用 ToList 来解决它,或者它可以作为分离不良的副产品,例如返回 IEnumerable 的存储库模式。
第二个例子稍微好一点,他们避免使用全读 ToList() 调用,但调用仍然会为不需要的数据加载整行。这会占用数据库和 Web 服务器上的资源。
第三个示例演示了精炼查询以仅返回消费者需要的绝对最少的数据。这样可以更好地利用数据库服务器上的索引和执行计划。
在负载下您可能面临的其他性能缺陷是延迟加载等问题。数据库将执行有限数量的并发请求,因此如果某些查询启动了额外的延迟加载请求,那么当没有加载时,这些请求会立即执行。但在负载下,它们与其他查询和延迟加载请求一起排队,这可能会限制数据拉取。
最终,您应该针对您的数据库运行 SQL 分析器,以捕获正在执行的 SQL 查询的种类和数量。在测试环境中执行时,请密切注意读取计数和 CPU 成本,而不是总执行时间。作为一般经验法则,更高的读取和 CPU 成本查询将更容易受到负载下执行时间爆裂的影响。它们需要更多资源来运行,并且“接触”更多行意味着更多等待行/表锁。
另一件需要注意的事情是超大型数据系统中的“繁重”查询,这些查询需要涉及很多行,例如报表,在某些情况下,还需要高度可定制的搜索查询。如果需要这些,您应该考虑规划您的数据库设计,以包括一个只读副本以运行报告或大型搜索表达式,以避免主数据库中的行锁定情况会降低典型读写查询的响应能力。
编辑:识别延迟加载查询。
这些显示在分析器中,您可以在其中查询顶级表,但随后会看到一些针对相关表的附加查询。
例如,假设您有一个名为 Order 的表,其中有一个名为 Product 的相关表、另一个名为 Customer 的表和另一个名为 Address 的交货地址。要读取某个日期范围内的所有订单,您会看到如下查询:
SELECT [OrderId], [Quantity], [OrderDate] [ProductId], [CustomerId], [DeliveryAddressId] FROM [dbo].[Orders] WHERE [OrderDate] >= '2019-01-01' AND [OrderDate] < '2020-01-01'
您只想加载订单并返回它们。
当序列化程序遍历字段时,它会找到引用的产品、客户和地址,并通过尝试读取这些字段,将导致延迟加载:
SELECT [CustomerId], [Name] FROM [dbo].[Customers] WHERE [CustomerId] = 22
SELECT [ProductId], [Name], [Price] FROM [dbo].[Products] WHERE [ProductId] = 1023
SELECT [AddressId], [StreetNumber], [City], [State], [PostCode] FROM [dbo].[Addresses] WHERE [AddressId] = 1211
如果您的原始查询返回 100 个订单,您可能会看到上述查询集的 100 倍,每个订单的一组作为延迟加载命中 1 个订单行将尝试按客户 ID 查找相关客户,相关产品 ID 和相关地址(交货地址 ID)。这可以而且确实代价高昂。在测试环境中运行时它可能不可见,但这会增加很多潜在的查询。
如果使用 .Include() 为相关实体进行预先加载,EF 将编写 JOIN 语句以一次性获取所有相关行,这比获取每个单独的相关实体要快得多。尽管如此,这可能会导致提取大量您不需要的数据。避免这种额外成本的最佳方法是利用通过Select 的投影来仅检索您需要的列。