一尘不染

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

sql

我遇到了我不知道的最奇怪的事情。我有一个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数据类型问题。这是一个很长的路要走,我将再次更新我发现的内容。

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

Edit3:我重新启动了所有内容,以包括开发IIS服务器,SQL服务器和开发笔记本电脑。现在它似乎正在工作。尽管这不是为什么发生的答案。我将开放此问题以解释发生了什么,我将其中之一标记为答案。

Edit4:话虽如此,我在不做任何更改的情况下又进行了一次测试,并且返回了相同的异常。我真的开始认为这是一个SQL问题。我正在更新此问题上的标签。我制作了一个单独的应用程序,该应用程序运行完全相同的查询,并且运行良好。

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

                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。我认为我真正需要的是为该过程提供更多内存的方法,但是我不知道在哪里配置它。

Edit7:我只是将开发服务器从使用本地计算机上的IIS Express更改为内置的Visual Studio
Web服务器。OOM异常现在消失了。我真的认为这是分配连续内存块的问题,无论出于何种原因,IIS
Express都不会将其分叉。现在它运行良好,我将在运行常规IIS7的2008R2上发布到功能完善的服务器,以了解其运行情况。


阅读 196

收藏
2021-03-17

共1个答案

一尘不染

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

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

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

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

2021-03-17