我正在开发一些企业应用程序,这些应用程序每天都要处理大量数据,为此,它使用C#.NET 4编写了WINDOWS SERVICE应用程序。它还连接到SQL SERVER 2008 R2,但由于某种原因,它(随机)在存储JSON序列化数据的同步表中向我抛出了这个错误:
Exception of type 'System.OutOfMemoryException' was thrown.
at System.Data.SqlClient.TdsParser.ReadPlpUnicodeChars(Char[]& buff, Int32 offst, Int32 len, TdsParserStateObject stateObj)
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.GetValueInternal(Int32 i)
at System.Data.SqlClient.SqlDataReader.GetValues(Object[] values)
此表是保存LOB数据的相当通用的表:
CREATE TABLE [dbo].[SyncJobItem](
[id_job_item] [int] IDENTITY(1,1) NOT NULL,
[id_job] [int] NOT NULL,
[id_job_item_type] [int] NOT NULL,
[id_job_status] [int] NOT NULL,
[id_c] [int] NULL,
[id_s] [int] NULL,
[job_data] [nvarchar](max) NOT NULL,
[last_update] [datetime] NOT NULL,
CONSTRAINT [PK_SyncJobItem] PRIMARY KEY CLUSTERED)
失败的LOB记录在job_data
列中有36.231.800个字符的数据,即(如果我们说1个字符是2个字节,UTF-8)大约70MB的数据,这并不多。
请考虑更改作业的数据存储(例如磁盘)或类似的东西对我来说不是一个选项。我想修复这个错误,所以如果有人知道什么,请帮忙!
同样,这个错误在相同的数据上随机发生,运行的系统是vmWare vCloud,我认为这是一些大型刀片系统。我们有大约6GB的RAM专用于我们的vm(服务最多使用大约1-2GB),服务被编译为x64,系统是x64 Windows 2008R2标准。我已经确保没有一个对象的内存超过2GB,所以事实并非如此,SqlClient内部也有错误,在我15年的开发经验中,我从未见过它,谷歌也没有发现任何结果。此外,错误不在DB端,因为DB有超过32GB的RAM,并且只使用20GB的峰值。对于我在这个系统中使用的不常见的细节,是在每个作业步骤之后使用多线程和GC.Collect()(在数据上有多个步骤)。
编辑:
这是造成这个问题的完整代码:
internal static void ExecuteReader(IConnectionProvider conn, IList destination, IObjectFiller objectBuilder, string cmdText, DbParameterCollection parameters, CommandType cmdType, int cmdTimeout)
{
IDbCommand cmd = CreateCommand(conn.DBMS, cmdText, parameters, cmdType, cmdTimeout);
cmd.Connection = conn.Connection;
bool connIsOpennedLocally = EnsureOpenConnection(conn);
try
{
AssignExistingPendingTransactionToCommand(conn, cmd);
using (IDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleResult))
{
objectBuilder.FillCollection(reader, destination);
PopulateOutputParameterValues(parameters, cmd);
}
}
finally
{
CloseConnectionIfLocal(conn, connIsOpennedLocally);
cmd.Dispose();
}
}
...
private void FillFromAlignedReader(ICollection<TEntity> collection, IDataReader openedDataReader, IDbTable table)
{
// Fastest scenario: data reader fields match entity field completely.
// It's safe to reuse same array because GetValues() always overwrites all members. Memory is allocated only once.
object[] values = new object[openedDataReader.FieldCount];
while (openedDataReader.Read())
{
openedDataReader.GetValues(values);
TEntity entity = CreateEntity(table, EntityState.Synchronized, values);
collection.Add(entity);
}
}
对于那些经过大量测试和MSDN(链接)后遇到此问题的人,我得出的结论是,在x64机器上,SqlDataReader
在正常读取模式下能够读取的最大单个字段大小约为70MB,之后需要将其SqlCommand
切换为CommandBehavior.SequentialAccess
并流式传输字段内容。
这样工作的示例代码:
...
behaviour = CommandBehavior.SequentialAccess;
using (IDataReader reader = cmd.ExecuteReader(behaviour))
{
filler.FillData(reader, destination);
}
当您在循环中读取数据时,您需要按顺序提取列,当您到达BLOB列时,您应该调用这样的东西(取决于数据类型):
...
private string GetBlobDataString(IDataReader openedDataReader, int columnIndex)
{
StringBuilder data = new StringBuilder(20000);
char[] buffer = new char[1000];
long startIndex = 0;
long dataReceivedCount = openedDataReader.GetChars(columnIndex, startIndex, buffer, 0, 1000);
data.Append(buffer, 0, (int)dataReceivedCount);
while (dataReceivedCount == 1000)
{
startIndex += 1000;
dataReceivedCount = openedDataReader.GetChars(columnIndex, startIndex, buffer, 0, 1000);
data.Append(buffer, 0, (int)dataReceivedCount);
}
return data.ToString();
}
private byte[] GetBlobDataBinary(IDataReader openedDataReader, int columnIndex)
{
MemoryStream data = new MemoryStream(20000);
BinaryWriter dataWriter = new BinaryWriter(data);
byte[] buffer = new byte[1000];
long startIndex = 0;
long dataReceivedCount = openedDataReader.GetBytes(columnIndex, startIndex, buffer, 0, 1000);
dataWriter.Write(buffer, 0, (int)dataReceivedCount);
while (dataReceivedCount == 1000)
{
startIndex += 1000;
dataReceivedCount = openedDataReader.GetBytes(columnIndex, startIndex, buffer, 0, 1000);
dataWriter.Write(buffer, 0, (int)dataReceivedCount);
}
data.Position = 0;
return data.ToArray();
}
这应该适用于1GB-1.5GB左右的数据,之后它将在单个对象无法保留足够大小的连续内存块时中断,因此要么从缓冲区直接刷新到磁盘,要么将数据拆分到多个较小的对象。
我认为对于这些大量的数据,应该使用数据库类型Text。只有当你需要对nvarchar进行搜索/点赞时才使用它。注意,当启用全文搜索时,这可能会产生奇怪的行为。