繁体   English   中英

从SqlDataReader读取字符串时内存不足

[英]Out of Memory when reading a string from SqlDataReader

我遇到了一些我无法弄清楚的最奇怪的事情。 我有一个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();

我尝试创建一个对象和一个stringbuilder来获取数据,但我仍然得到相同的内存不足异常。 我也尝试过使用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数据类型问题。 这是一个很长的镜头,我会用我发现的东西再次更新。

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

Edit3:我重启了一切,包括我的开发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);

Edit6:添加到此,当OOM异常发生时,我看到IIS工作进程(它保存正在运行的方法)命中几乎700MB。 这是在IIS Express上运行的,而不是生产服务器上的完整IIS。 这与它有什么关系吗? 此外,当我调用Byte [] data = stream.ToArray()时,我也断断续续地获取OOM。 我认为我真正需要的是一种为这个过程提供更多内存的方法,但我不知道在哪里配置它。

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

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

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

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

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

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

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

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

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

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

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

这里疯狂猜测。

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

你错过了@符号。 所以它用ID替换了CompiledReportTimeID的两个实例,并且由于相等而得到所有结果?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM