我遇到了我无法弄清楚的最奇怪的事情。我有一个 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 数据类型问题。这是一个很长的镜头,我会再次更新我的发现。
Edit2:将表中的字段更改为 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 上的完整服务器,看看它是如何运行的。