这两天遇到一个奇怪的问题,通过 EF/EF Core 查询数据库速度奇慢,先是在传统的 ASP.NET 项目中遇到(用的是EF6.0),后来将该项目迁移至
ASP.NET Core 也是同样的问题(用的是EF Core 2.2.2)。


问题触发的条件是所查询的字段中存储了很大的字符串(有400多万个字符),查询耗时竟然要40s左右(对,是40秒),CPU消耗也很高,2核CPU消耗50%-80%左右,而换成
Dapper 则没这个问题。

通过 EF Core 的 Debug 日志跟踪发现,耗时发生在执行完 DbCommand 与 dispose DbDataReader
<https://github.com/aspnet/EntityFrameworkCore/blob/release/2.2/src/EFCore.Relational/Storage/RelationalDataReader.cs#L138>
之间:
2019-02-23 15:46:27.026 [Information] Executed DbCommand ("4"ms)
[Parameters=[""], CommandType='Text', CommandTimeout='30']" 2019-02-23
15:47:06.859 [Debug] A data reader was disposed.
通过日志跟踪信息看,很容易会怀疑耗时可能发生在 ADO.NET DataReader 读取数据时,但这个怀疑与 Dapper 查询正常矛盾,而且 CPU
消耗高也说明耗时不是出现在 IO 层面。

后来在 stackoverflow 上找到了线索 Poor performance when loading entity with large
string property using Entity Framework
<https://stackoverflow.com/questions/40496012/poor-performance-when-loading-entity-with-large-string-property-using-entity-fra>

I had the same issue yesterday. What I did find out is that async operations
with Entity Framework is broken or at least very slow. Try using the same
operations synchronously

当时看到了这个线索,有点不相信,异步竟然会引起这个问题,不是默认都使用异步吗?只是抱着试试看的心理将代码中的 ToListAsync() 改为
ToList() ,结果却让人大吃一惊,多次测试,查询耗时在 100-500 ms 之间,快了 100 多倍。

更新
触发这个问题有 3 个条件:
1)读取的字符串很大
2)使用 DbCommand.ExecuteReaderAsync 异步方法读取
3)调用 ExecuteReaderAsync 时没有给 behavior 参数传值 CommandBehavior.SequentialAccess

在 Dapper 中没有出现问题是因为 Dapper 中设置了 CommandBehavior.SequentialAccess ,详见 Dapper
的源代码SqlMapper.Async.cs#L945
<https://github.com/StackExchange/Dapper/blob/4e62055c5775aced61b848282ed180de0ab62161/Dapper/SqlMapper.Async.cs#L945>
using (var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed,
CommandBehavior.SequentialAccess | CommandBehavior.SingleResult,
command.CancellationToken).ConfigureAwait(false)) { //... }
EF Core 中会出现这个问题是因为 EF Core 调用的是 ExecuteReaderAsync(CancellationToken
cancellationToken) ,没有设置 CommandBehavior ,详见 EF Core 的源代码
RelationalCommand.cs#L292
<https://github.com/aspnet/EntityFrameworkCore/blob/release/2.2/src/EFCore.Relational/Storage/Internal/RelationalCommand.cs#L292>
result = new RelationalDataReader( connection, dbCommand, await
dbCommand.ExecuteReaderAsync(cancellationToken), commandId, Logger);
关于 CommandBehavior.SequentialAccess 详见微软官方文档
<https://docs.microsoft.com/en-us/dotnet/api/system.data.commandbehavior?redirectedfrom=MSDN&view=netcore-2.2>

Provides a way for the DataReader to handle rows that contain columns with
large binary values. Rather than loading the entire row, SequentialAccess
enables the DataReader to load data as a stream. You can then use the GetBytes
or GetChars method to specify a byte location to start the read operation, and
a limited buffer size for the data being returned.

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信