這兩天遇到一個奇怪的問題,經過 EF/EF Core 查詢數據庫速度奇慢,先是在傳統的 ASP.NET 項目中遇到(用的是EF6.0),後來將該項目遷移至 ASP.NET Core 也是一樣的問題(用的是EF Core 2.2.2)。git
問題觸發的條件是所查詢的字段中存儲了很大的字符串(有400多萬個字符),查詢耗時居然要40s左右(對,是40秒),CPU消耗也很高,2核CPU消耗50%-80%左右,而換成 Dapper 則沒這個問題。github
經過 EF Core 的 Debug 日誌跟蹤發現,耗時發生在執行完 DbCommand 與 dispose DbDataReader 之間:數據庫
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 層面。api
後來在 stackoverflow 上找到了線索 Poor performance when loading entity with large string property using Entity Frameworkapp
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 多倍。async
更新
觸發這個問題有 3 個條件:
1)讀取的字符串很大
2)使用 DbCommand.ExecuteReaderAsync
異步方法讀取
3)調用 ExecuteReaderAsync
時沒有給 behavior 參數傳值 CommandBehavior.SequentialAccess
ide
在 Dapper 中沒有出現問題是由於 Dapper 中設置了 CommandBehavior.SequentialAccess
,詳見 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日誌
result = new RelationalDataReader( connection, dbCommand, await dbCommand.ExecuteReaderAsync(cancellationToken), commandId, Logger);
關於 CommandBehavior.SequentialAccess 詳見微軟官方文檔
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.