【问题标题】:Out of Memory when reading a string from SqlDataReader从 SqlDataReader 读取字符串时内存不足
【发布时间】:2013-02-13 23:09:41
【问题描述】:

我遇到了一件我想不出来的最奇怪的事情。我有一个 SQL 表,其中有一堆存储在 ntext 字段中的报告。当我将其中一个的值复制并粘贴到记事本中并保存时(使用 Visual Studio 从不同行的较小报告中获取值),原始 txt 文件大约为 5Mb。当我尝试使用 SqlDataReader 获取相同的数据并将其转换为字符串时,出现内存不足异常。以下是我的尝试:

string output = "";
string cmdtext = "SELECT ReportData FROM Reporting_Compiled WHERE CompiledReportTimeID = @CompiledReportTimeID";
SqlCommand cmd = new SqlCommand(cmdtext, conn);
cmd.Parameters.Add(new SqlParameter("CompiledReportTimeID", CompiledReportTimeID));
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
    output = reader.GetString(0); // <--- exception happens here
}
reader.Close();

我尝试创建一个对象和一个字符串生成器来获取数据,但我仍然遇到同样的内存不足异常。我也尝试过使用 reader.GetValue(0).ToString() 也无济于事。该查询只返回 1 行,当我在 SQL Management Studio 中运行它时,它会非常开心。

抛出的异常是:

System.OutOfMemoryException was unhandled by user code  
Message=Exception of type 'System.OutOfMemoryException' was thrown.  
Source=mscorlib  
 StackTrace:  
 at System.String.CreateStringFromEncoding(Byte* bytes, Int32 byteLength, Encoding       encoding)  
   at System.Text.UnicodeEncoding.GetString(Byte[] bytes, Int32 index, Int32 count)  
   at System.Data.SqlClient.TdsParserStateObject.ReadString(Int32 length)  
   at System.Data.SqlClient.TdsParser.ReadSqlStringValue(SqlBuffer value, Byte type, Int32 length, Encoding encoding, Boolean isPlp, TdsParserStateObject stateObj)  
   at System.Data.SqlClient.TdsParser.ReadSqlValue(SqlBuffer value, SqlMetaDataPriv md, Int32 length, TdsParserStateObject stateObj)  
   at System.Data.SqlClient.SqlDataReader.ReadColumnData()  
   at System.Data.SqlClient.SqlDataReader.ReadColumn(Int32 i, Boolean setTimeout)  
   at System.Data.SqlClient.SqlDataReader.GetString(Int32 i)  
   at Reporting.Web.Services.InventoryService.GetPrecompiledReportingData(DateTime ReportTime, String ReportType) in   C:\Projects\Reporting\Reporting.Web\Services\InventoryService.svc.cs:line 3244  
   at SyncInvokeGetPrecompiledReportingData(Object , Object[] , Object[] )  
   at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)  
   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)  
 InnerException:   
    null

我使用其他行号进行了测试,这些行号似乎有效,但这是误报,因为这些测试 ID 没有数据。在查看包含几乎相同的报告的表格后,我提取了其他一些测试 ID,并且我得到了相同的异常。也许它是如何编码字符串的?存储在表中的数据是一个 JSON 编码的字符串,它是由我在其他地方创建的一个非常粗糙的类生成的,以防万一。

这是前面的代码块:

// get the report time ID
int CompiledReportTimeTypeID = CompiledReportTypeIDs[ReportType];
int CompiledReportTimeID = -1;
cmdtext = "SELECT CompiledReportTimeID FROM Reporting_CompiledReportTime WHERE CompiledReportTimeTypeID = @CompiledReportTimeTypeID AND CompiledReportTime = @ReportTime";
cmd = new SqlCommand(cmdtext, conn);
cmd.Parameters.Add(new SqlParameter("CompiledReportTimeTypeID", CompiledReportTimeTypeID));
cmd.Parameters.Add(new SqlParameter("ReportTime", ReportTime));
reader = cmd.ExecuteReader();
while (reader.Read())
{
    CompiledReportTimeID = Convert.ToInt32(reader.GetValue(0));
}
reader.Close();

CompiledReportTypeIDs 是一个字典,它根据在方法开头输入的字符串参数获取正确的 CompiledReportTimeTypeID。 ReportTime 是一个较早输入的 DateTime。

编辑: 我将删除该表并使用 ReportData 字段作为 nvarchar(MAX) 而不是 ntext 重新创建它,以排除 SQL 数据类型问题。这是一个很长的镜头,我会再次更新我的发现。

编辑2: 将表中的字段更改为 nvarchar(max) 无效。我也尝试使用 output = cmd.ExecuteScalar().ToString() ,没有任何影响。我正在尝试查看 SqlDataReader 是否有最大大小。当我从 SQL Mgmt Studio 复制文本的值时,保存在记事本中时只有 43Kb。为了验证这一点,我提取了一个具有已知工作 ID 的报告(一个较小的报告),当我直接从 Visual Studio 中复制该值并将其转储到记事本中时,它大约为 5MB!这意味着这些大报告可能在 nvarchar(max) 字段中的 ~20MB 范围内。

编辑3: 我重新启动了所有东西,包括我的开发 IIS 服务器、SQL 服务器和我的开发笔记本电脑。现在它似乎正在工作。这不是为什么会发生这种情况的答案。我将保留这个问题,以便对发生的事情进行解释,并将其中一个标记为答案。

编辑4: 话虽如此,我在没有更改任何内容的情况下运行了另一个测试,并且返回了相同的异常。我真的开始认为这是一个 SQL 问题。我正在更新这个问题的标签。我制作了一个单独的应用程序,它运行完全相同的查询并且运行良好。

编辑5: 我已经按照以下答案之一实现了顺序访问。一切都被正确地读入流中,但是当我尝试将其写入字符串时,我仍然遇到内存不足异常。这是否表明获取连续内存块的问题?这是我实现缓冲的方式:

                reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
            long startIndex = 0;
            long retval = 0;
            int bufferSize = 100;
            byte[] buffer = new byte[bufferSize];
            MemoryStream stream = new MemoryStream();
            BinaryWriter writer = new BinaryWriter(stream);
            while (reader.Read())
            {
                // Reset the starting byte for the new CLOB.
                startIndex = 0;

                // Read bytes into buffer[] and retain the number of bytes returned.
                retval = reader.GetBytes(0, startIndex, buffer, 0, bufferSize);

                // Continue while there are bytes beyond the size of the buffer.
                while (retval == bufferSize)
                {
                    writer.Write(buffer);
                    writer.Flush();

                    // Reposition start index to end of last buffer and fill buffer.
                    startIndex += bufferSize;
                    retval = reader.GetBytes(0, startIndex, buffer, 0, bufferSize);
                }

                //output = reader.GetString(0);
            }
            reader.Close();
            stream.Position = 0L;
            StreamReader sr = new StreamReader(stream);
            output = sr.ReadToEnd(); <---- Exception happens here
            //output = new string(buffer);

编辑6: 除此之外,当发生 OOM 异常时,我看到 IIS 工作进程(它保存正在运行的方法)几乎达到 700MB。这是在 IIS Express 上运行的,而不是在生产服务器上的完整 IIS。这和它有什么关系吗?此外,当我调用 Byte[] data = stream.ToArray() 时,我也会间歇性地得到 OOM。我认为我真正需要的是一种为这个进程提供更多内存的方法,但我不知道在哪里配置它。

编辑7: 我刚刚将我的开发服务器从在本地计算机上使用 IIS Express 更改为内置的 Visual Studio Web 服务器。 OOM 异常现在消失了。我真的认为这是分配一个连续的内存块问题,无论出于何种原因,IIS Express 都不会分叉它。现在它运行良好,我将发布到运行常规 IIS7 的 2008R2 上的完整服务器,看看它是如何运行的。

【问题讨论】:

  • 您也应该包含完整的错误消息。
  • 返回的字符串有多大?换句话说,ReportData 有多大?
  • 显示异常的完整堆栈跟踪。
  • 你可以在 SqlCommandSqlDataReaderSqlConnection 对象周围尝试使用 using 块。
  • 从 SSMS 复制大字符串不是一个可靠的方法,因为 SSMS 设置了它返回的字符串大小的上限。如果您想在 SSMS 中可靠地测量字符串长度,请将 Len(col) 添加到您的查询中。

标签: c# sql performance memory


【解决方案1】:

您应该在执行阅读器时尝试通过指定command behavior 顺序读取数据。根据文档,使用 SequentialAccess 检索大值和二进制数据。否则,可能会发生 OutOfMemoryException 并关闭连接

虽然顺序访问通常用于大型二进制数据,但根据 MSDN 文档,您也可以使用它来读取大量字符数据。

访问 BLOB 字段中的数据时,使用 GetBytes 或 DataReader 的 GetChars 类型访问器,它用 数据。您也可以使用 GetString 获取字符数据;然而。到 节省您可能不想加载整个 BLOB 的系统资源 值转换为单个字符串变量。您可以改为指定一个 要返回的数据的特定缓冲区大小,以及起始位置 从返回的数据中读取第一个字节或字符。 GetBytes 和 GetChars 将返回一个 long 值,它表示 返回的字节数或字符数。如果您将空数组传递给 GetBytes 或 GetChars,返回的 long 值将是总数 BLOB 中的字节数或字符数。您可以选择指定一个 数组中的索引作为正在读取的数据的起始位置。

这个MSDN example 展示了如何执行顺序访问。我相信你可以使用GetChars方法来读取文本数据。

【讨论】:

  • 这听起来很有希望。我今天需要坐飞机,但我会在早上尝试第一件事。
  • 缓冲效果很好,但是在尝试将创建的流写入字符串时出现 OOM 异常。当我使用 .GetChars() 而不是 .GetBytes() 时,我会立即获得 OOM,因为我试图获取字段的长度来实例化包含结果的 char 数组。
  • 您可以使用 DATALENGTH 将总长度作为结果集的一部分返回,然后在以块的形式读取结果之前使用该值构造数组。
  • 我会试试这个!我下周之前都不在办公室,但我会在星期一早上尝试第一件事。
  • 我认为您可能还需要读取缓冲区中的流,而不是使用 ReadToEnd()。祝你好运。
【解决方案2】:

从根本上说,System.OutOfMemoryException 不仅会在内存不足时发生,而且会在您无法为对象分配单个连续内存块时发生。在尝试创建非常大的数组、加载大型位图对象时,或者有时在创建大型 XmlDocuments 时,您经常会看到该错误...

ArrayString 通常需要连续分配,即不能被分解成碎片并分配到内存中的空白空间中。

这可能不是 SQL 问题,而更多的是 SqlReader 尝试分配足够大的字符串以包含连续数据的问题。

您提到它在重新启动后正常工作,所以让我们假设您的代码基本正确(可能仍然可以优化以将数据公开为流而不是缓冲记录集)并且当前症状是环境问题。刚重新启动的机器可能没有那么多的碎片内存,但是随着您使用的更多,内存碎片并返回错误...

也许可以通过关闭尽可能多的其他程序来证明连续内存理论,并在错误代码之前添加代码以强制GC.Collect(GC.MaxGeneration) (reference)。这不是保证,因为分配给您的进程的内存可能仍然是碎片化的。

我认为流式传输值可能是阻止错误发生的方法,并且最好避免尝试将所有内容缓冲到字符串中。这样做的缺点是您将保持数据库连接打开,而结果由程序的其余部分流式传输/使用,这将带来自己的开销。我不确定您的代码需要对结果做什么,但如果它需要与 String 实例一起使用,您可能需要扩展进程可用的内存(有几种方法可以帮助解决这个问题,但可能会关闭-topic - 发表评论,如果需要,我可以添加到这个答案中)

【讨论】:

  • 我尝试强制 GC 无济于事(不过是个好主意!)。我根据反对者的回答实现了缓冲,当我尝试将流转储到字符串时,我得到了 OOM。这使我同意内存分配问题。您是否有指向我可以遵循以扩展可用进程内存的指南的链接?当然,我应该找到一种方法来做我需要的事情,但现在这样的事情会起作用。
  • 我真的不建议尝试调整环境设置以使内存正常工作。您可以选择将数据流式传输到目的地吗?例如想象一下,你是两座大坝之间的泵站,你不能在将一个大坝中的所有​​水抽入另一个大坝之前将其吸干。您必须一次刷新一个缓冲区。我认为问题在于试图将所有数据转储到一个字符串中 - 最好避免这样做。
【解决方案3】:

在这里猜测一下。

cmd.Parameters.Add(new SqlParameter("CompiledReportTimeID", CompiledReportTimeID));

你错过了@符号。所以它用 id 替换了 CompiledReportTimeID 的两个实例,你得到所有的结果,而不是因为相等?

【讨论】:

  • 如果我将 @ 添加到 SQL 参数的第一个 arg 中,我仍然会得到相同的结果。 Stack Overflow 对 CompiledReportTimeID 进行了有趣的格式化,但它只是一个 int。我将参数命名为与 int 相同的名称,它也恰好与 table 中的字段名称相同。以这种方式命名它可能不是最佳实践(一旦我得到这个愚蠢的东西,我会稍后修复它)
猜你喜欢
  • 1970-01-01
  • 2012-05-13
  • 2021-09-13
  • 1970-01-01
  • 2016-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-30
相关资源
最近更新 更多