2013-02-27 110 views
13

我運行到我想不通奇怪的事情字符串時。我有一個存儲在ntext字段中的一堆報表的SQL表。當我將其中一個文件的值複製並粘貼到記事本中並將其保存(使用Visual Studio從不同行中的較小報告中獲取值)時,原始txt文件大約爲5Mb。當我嘗試使用SqlDataReader獲取相同的數據並將其轉換爲字符串時,出現內存不足異常。這裏是我正在試圖做到這一點:內存讀取從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是一個早先提供的日期時間。

編輯: 爲了排除SQL數據類型問題,我將刪除表並使用ReportData字段將其重新創建爲nvarchar(MAX)而不是ntext。這是一個長鏡頭,我會再次更新我發現的內容。

EDIT2: 更改在表中爲nvarchar(最大)領域沒有任何影響。我也嘗試使用output = cmd.ExecuteScalar()。ToString()以及沒有影響。我試圖看看SqlDataReader是否有最大尺寸。當我從SQL Mgmt Studio中複製文本的值時,它在保存在記事本中時只有43Kb。爲了驗證這一點,我用一個已知的工作ID(一個較小的報告)提取了一份報告,當我將這個值直接從Visual Studio中拷貝出來並將它轉儲到記事本中時,它大約爲5MB!這意味着這些大的報告可能位於nvarchar(max)字段的〜20MB範圍內。

EDIT3: 我重新啓動一切,包括我的dev IIS服務器,SQL服務器和我的dev的筆記本電腦。現在它似乎在工作。這不是爲什麼發生這種情況的答案。我將這個問題留待解釋發生了什麼,我將其中的一個標記爲答案。

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。我認爲我真正需要的是爲這個過程提供更多內存的方式,但我不知道在哪裏配置。

編輯7: 我剛剛在我的本地計算機上使用IIS Express將我的開發服務器更改爲內置的Visual Studio Web服務器。 OOM異常現在消失了。我真的認爲這是分配一塊連續的內存問題,無論出於何種原因,IIS Express都不會將其分叉。現在它運行良好,我將在運行常規IIS7的2008R2上發佈到我的完整服務器,以查看它是如何發生的。

+1

您還應該包含完整的錯誤消息。 – 2013-02-27 22:50:31

+1

正在返回的字符串有多大?換句話說,ReportData有多大? – 2013-02-27 22:50:57

+0

顯示異常的完整堆棧跟蹤。 – 2013-02-27 22:58:13

回答

9

你應該嘗試通過指定command behavior當你執行讀寫器讀取數據的順序。根據文檔,使用SequentialAccess檢索較大的值和二進制數據。否則,可能會發生OutOfMemoryException,並且連接將被關閉

雖然順序訪問通常用於大型二進制數據,根據你可以用它來讀取大量的字符數據,以及MSDN文檔上。

在BLOB字段訪問數據時,使用輸入的的DataReader的存取器GetBytes會或 則GetChars,其填充 數據的數組。你也可以使用GetString作爲字符數據;然而。到 節省系統資源,您可能不希望將整個BLOB 值加載到單個字符串變量中。您可以改爲指定要返回的數據的特定緩衝區大小,以及要從返回的數據中讀取第一個字節或字符的起始位置 。 GetBytes和GetChars將返回一個長整型值,它表示返回的字節數或字符數 。如果將空數組傳遞給 GetBytes或GetChars,則返回的long值將是BLOB中字節或字符的總數 。您可以選擇指定數組中的 索引作爲正在讀取的數據的起始位置。

MSDN example顯示如何執行順序訪問。我相信你可以使用GetChars方法來讀取文本數據。

+0

這聽起來很有希望。我需要一整天的時間,但我會在早上第一件事。 – 2013-02-28 22:56:04

+0

緩衝效果很好,但是當我嘗試寫入字符串的流時遇到了OOM異常。當我使用.GetChars()而不是.GetBytes()時,我會立即得到OOM,因爲我試圖獲取字段的長度來實例化包含結果的char數組。 – 2013-03-01 18:23:59

+0

您可以使用DATALENGTH作爲結果集的一部分返回總長度,然後在以塊讀取結果之前使用該值構造數組。 – Oppositional 2013-03-04 04:59:16

0

這裏瘋狂猜測。

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

您錯過了@符號。所以它將CompiledReportTimeID的兩個實例替換爲id,並且因爲相等而取得所有結果?

+0

如果我將@添加到SQL參數的第一個參數中,我仍然會得到相同的結果。 Stack Overflow格式化CompiledReportTimeID很有趣,但它只是一個int。我將該參數命名爲與int相同的東西,這也恰好與表中的字段名稱相同。可能不是一種最好的方法來命名它(一旦我得到這個愚蠢的事情,我會修復它) – 2013-02-28 21:09:02

4

從根本上講,System.OutOfMemoryException並不僅僅當你的內存不足發生,但是當不能爲對象分配存儲器的單個連續塊。在嘗試創建非常大的陣列,或加載大型位圖對象時,或者有時在創建大型XmlDocuments時,您經常會看到該錯誤...

ArrayString通常需要連續分配,即不能被分解成片段並分配到內存中的空白區域。

這可能不是SQL問題,更多的是SqlReader嘗試分配足夠大的字符串以包含連續數據的問題。

你提到它在重新啓動後能夠正常工作,所以我們假設你的代碼基本上是正確的(可能仍然可以優化爲將數據暴露爲流而不是緩衝記錄集),並且當前症狀是環境。剛剛重新啓動的機器可能沒有儘可能多的碎片內存,但隨着您使用更多,內存碎片化和錯誤返回...

可能能夠證明連續記憶理論,儘可能多的其他程序,並添加代碼,以便在出現錯誤代碼之前強制執行GC.Collect(GC.MaxGeneration)reference)。這不是一個保證,因爲分配給你的進程的內存可能仍然是碎片。

我認爲流的值可能是阻止錯誤發生的方法,更好地避免嘗試將所有內容緩衝到字符串中。這樣做的缺點是你將保持數據庫連接打開,而結果被其他程序流式傳輸/消耗,並且會帶來自己的開銷。我不確定你的代碼需要怎樣處理結果,但是如果它需要與String實例一起工作,則可能需要擴展該進程可用的內存(有幾種方法可以提供幫助,但可能是無關緊要的 - 留下評論,如果需要,我可以添加到此答案)

+0

我試圖迫使GC無濟於事(雖然好主意!)。我根據Oppositional的回答實現了緩衝,當我嘗試將流轉儲爲字符串時,我得到了OOM。這導致我同意內存分配問題。你有鏈接到我可以遵循的指南來擴展可用的進程內存嗎?當然,我應該找到一種方法去做我需要的東西,但是現在這樣的事情會起作用。 – 2013-03-01 18:25:35

+0

我真的建議不要試圖調整環境設置以使內存正常工作。你有選擇將數據流式傳輸到目的地嗎?例如想象你是兩座水壩之間的泵站,在將它抽入另一個水壩之前,你不能吸收一個水壩中的所有​​水。您必須一次沖洗一個緩衝區。我認爲問題是試圖將所有數據轉儲到一個字符串中 - 最好避免這樣做。 – 2013-03-06 12:37:25