如何将.NET对象的大graphics序列化为SQL Server BLOB而不创build大型缓冲区?

我们有这样的代码:

ms = New IO.MemoryStream bin = New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bin.Serialize(ms, largeGraphOfObjects) dataToSaveToDatabase = ms.ToArray() // put dataToSaveToDatabase in a Sql server BLOB 

但内存蒸汽从大内存堆中分配一个大的缓冲区 ,这给我们带来了问题。 那么我们如何在不需要足够的空闲内存来存放序列化对象的情况下stream式传输数据。

我正在寻找一种方法从SQL服务器获取stream,然后可以传递给bin.Serialize(),以避免保留我的进程内存中的所有数据。

同样为了读取数据…


一些更多的背景。

这是一个复杂的数字处理系统的一部分,它可以近乎实时地处理数据,查找设备问题等等,当数据馈送等数据质量出现问题时,序列化可以重新启动。(我们存储数据馈送并在操作员编辑出不良值后重新运行。)

因此,我们更经常地序列化对象,然后我们对它们进行反序列化。

我们正在序列化的对象包括非常大的数组,主要是双打以及许多小的“更普通”的对象。 我们正在推动32位系统的内存限制,并使车库collections家非常努力。 (系统中的其他地方正在改进效果,例如重新使用大型数组,而不是创build新的数组。

通常情况下,国家的系列化是解决记忆exception的最后一根稻草 ; 我们的峰值内存使用量是在这个序列化完成的时候。

认为当我们对序列化对象的时候,我们会得到大的内存池碎片,我想在给定数组大小的情况下,还有其他大内存池碎片的问题。 (这个还没有被调查过,因为第一个看这个的人是数值处理专家,而不是内存pipe理专家。)

客户是否使用Sql Server 2000,2005和2008的混合体,如果可能的话,我们宁愿每个版本的Sql Server都没有不同的代码path。

我们可以同时有多个活动模型(在不同的过程中,跨越多台机器),每个模型可以有许多保存的状态。 因此,保存的状态存储在数据库blob而不是文件中。

由于保存状态的传播很重要,所以我宁愿将对象序列化为一个文件,然后将这个文件一次放入一个BLOB块中。

其他相关的问题,我问过

  • 如何从/到SQL Server BLOB字段stream数据?
  • 是否有一个像Sql Server 2005一样的SqlFileStream类?

没有内置的ADO.Netfunction可以很好地处理大数据。 问题有两个:

  • 没有任何API可以将SQL命令或参数写入到stream中。 接受stream的参数types(如FileStream )接受从它读取的stream,这不符合写入stream的序列化语义。 不pipe你用哪种方式来做这件事,最终都会得到整个序列化对象的内存拷贝,不好的。
  • 即使上面的要点被解决(而且不可能),TDS协议和SQL Server接受参数的方式也不能很好地处理大参数,因为整个请求必须在启动之前首先被接收,在SQL Server中创build对象的其他副本。

所以你必须从不同的angular度来看待这个问题。 幸运的是,有一个相当简单的解决scheme。 诀窍是使用高效的UPDATE .WRITE语法,并在一系列T-SQL语句中逐个传递数据块。 这是MSDN推荐的方法,请参阅修改ADO.NET中的大值(最大值)数据 。 这看起来很复杂,但是实际上并不重要,并且可以插入到Stream类中。


BlobStream类

这是解决scheme的面包和黄油。 一个Stream派生类,它实现Write方法作为对T-SQL BLOB WRITE语法的调用。 直截了当地说,唯一有趣的是它必须跟踪第一次更新,因为UPDATE ... SET blob.WRITE(...)语法将在NULL字段上失败:

 class BlobStream: Stream { private SqlCommand cmdAppendChunk; private SqlCommand cmdFirstChunk; private SqlConnection connection; private SqlTransaction transaction; private SqlParameter paramChunk; private SqlParameter paramLength; private long offset; public BlobStream( SqlConnection connection, SqlTransaction transaction, string schemaName, string tableName, string blobColumn, string keyColumn, object keyValue) { this.transaction = transaction; this.connection = connection; cmdFirstChunk = new SqlCommand(String.Format(@" UPDATE [{0}].[{1}] SET [{2}] = @firstChunk WHERE [{3}] = @key" ,schemaName, tableName, blobColumn, keyColumn) , connection, transaction); cmdFirstChunk.Parameters.AddWithValue("@key", keyValue); cmdAppendChunk = new SqlCommand(String.Format(@" UPDATE [{0}].[{1}] SET [{2}].WRITE(@chunk, NULL, NULL) WHERE [{3}] = @key" , schemaName, tableName, blobColumn, keyColumn) , connection, transaction); cmdAppendChunk.Parameters.AddWithValue("@key", keyValue); paramChunk = new SqlParameter("@chunk", SqlDbType.VarBinary, -1); cmdAppendChunk.Parameters.Add(paramChunk); } public override void Write(byte[] buffer, int index, int count) { byte[] bytesToWrite = buffer; if (index != 0 || count != buffer.Length) { bytesToWrite = new MemoryStream(buffer, index, count).ToArray(); } if (offset == 0) { cmdFirstChunk.Parameters.AddWithValue("@firstChunk", bytesToWrite); cmdFirstChunk.ExecuteNonQuery(); offset = count; } else { paramChunk.Value = bytesToWrite; cmdAppendChunk.ExecuteNonQuery(); offset += count; } } // Rest of the abstract Stream implementation } 

使用BlobStream

要使用这个新创build的blobstream类,可以插入BufferedStream 。 该类有一个简单的devise,只处理写入表中的一列。 我将从另一个例子中重用一个表格:

 CREATE TABLE [dbo].[Uploads]( [Id] [int] IDENTITY(1,1) NOT NULL, [FileName] [varchar](256) NULL, [ContentType] [varchar](256) NULL, [FileData] [varbinary](max) NULL) 

我将添加一个虚拟对象进行序列化:

 [Serializable] class HugeSerialized { public byte[] theBigArray { get; set; } } 

最后是实际的序列化。 我们将首先在Uploads表中插入一条新logging,然后在新插入的Id上创build一个BlobStream ,并直接调用序列化到这个stream中:

 using (SqlConnection conn = new SqlConnection(Settings.Default.connString)) { conn.Open(); using (SqlTransaction trn = conn.BeginTransaction()) { SqlCommand cmdInsert = new SqlCommand( @"INSERT INTO dbo.Uploads (FileName, ContentType) VALUES (@fileName, @contentType); SET @id = SCOPE_IDENTITY();", conn, trn); cmdInsert.Parameters.AddWithValue("@fileName", "Demo"); cmdInsert.Parameters.AddWithValue("@contentType", "application/octet-stream"); SqlParameter paramId = new SqlParameter("@id", SqlDbType.Int); paramId.Direction = ParameterDirection.Output; cmdInsert.Parameters.Add(paramId); cmdInsert.ExecuteNonQuery(); BlobStream blob = new BlobStream( conn, trn, "dbo", "Uploads", "FileData", "Id", paramId.Value); BufferedStream bufferedBlob = new BufferedStream(blob, 8040); HugeSerialized big = new HugeSerialized { theBigArray = new byte[1024 * 1024] }; BinaryFormatter bf = new BinaryFormatter(); bf.Serialize(bufferedBlob, big); trn.Commit(); } } 

如果你监视这个简单示例的执行,你将会看到没有创build一个大的序列化stream。 示例将分配[1024 * 1024]的数组,但这是为了演示的目的有序列化的东西。 此代码按缓冲方式按块大小进行序列化,一次使用SQL Server BLOBbuild议的更新大小8040字节。

所有你需要的是.NET Framework 4.5和stream媒体。 假设我们在HDD上有一个大文件,我们想要上传这个文件。

SQL代码:

 CREATE TABLE BigFiles ( [BigDataID] [int] IDENTITY(1,1) NOT NULL, [Data] VARBINARY(MAX) NULL ) 

C#代码:

 using (FileStream sourceStream = new FileStream(filePath, FileMode.Open)) { using (SqlCommand cmd = new SqlCommand(string.Format("UPDATE BigFiles SET Data=@Data WHERE BigDataID = @BigDataID"), _sqlConn)) { cmd.Parameters.AddWithValue("@Data", sourceStream); cmd.Parameters.AddWithValue("@BigDataID", entryId); cmd.ExecuteNonQuery(); } } 

对我有好处。 我已经成功地上传了400 MB的文件,而当我尝试将这个文件加载到内存中时,MemoryStream抛出了一个exception。

UPD:此代码适用于Windows 7,但在Windows XP和2003 Server上失败。

您始终可以使用Microsoft自第一天起使用的有线协议TDS(表格数据stream)在较低级别写入SQL Server。 即使SQLAzure使用它,它们也不可能很快改变!

你可以从Mono项目和freetds项目中看到这个工作原理

看看tds_blob

图表是什么样的?

这里的一个问题是溪stream。 SQL 2005的要求是一个痛苦,否则,你可以直接写入SqlFileStream ,但是,我不认为这会是很难编写自己的Stream实现,缓冲8040(或一些多个)字节,并增量写入。 然而,我不确定这是否值得这个额外的复杂性 – 我会非常想用一个文件作为暂存缓冲区, 然后 (一旦序列化)循环的文件插入/附加块。 我不认为文件系统会在这里损害你的整体性能,而且也会使你无法开始编写注定要失败的数据 – 也就是说,除非你已经知道你要写什么数据,否则不会和数据库通信。 它还将帮助您最大限度地减less连接打开的时间。

接下来的问题是序列化本身。 就个人而言,我build议使用BinaryFormatter写入持久性存储(仅用于传输),因为它在编码器本身和types中都是特定实现的(即,如果对数据types进行无辜修改)。

如果你的数据可以充分performance为一个 (而不是一个完整的图),我会很想尝试协议缓冲区/ protobuf-net。 这种编码(由Googledevise)小于BinaryFormatter输出,读写速度更快,而且是基于合同而不是基于字段的,因此您可以在以后再次可靠地重新使用它(即使您完全切换平台)。

默认选项意味着它必须在每个对象之前写入对象长度(在你的情况下这可能是昂贵的),但是如果你有嵌套的大(深)对象列表,你可以使用分组编码来避免这种需要 – 允许它以只向前,单向的方式写入stream; 这里有一个使用分组编码的简单示例,但是如果您想对我施加更复杂的场景,请告诉我…

 using System; using System.Collections.Generic; using System.IO; using ProtoBuf; [ProtoContract] public class Foo { private readonly List<Bar> bars = new List<Bar>(); [ProtoMember(1, DataFormat = DataFormat.Group)] public List<Bar> Bars { get { return bars;}} } [ProtoContract] public class Bar { [ProtoMember(1)] public int Id { get; set; } [ProtoMember(2)] public string Name { get; set; } } static class Program { static void Main() { var obj = new Foo { Bars = { new Bar { Id = 123, Name = "abc"}, new Bar { Id = 456, Name = "def"}, } }; // write it and show it using (MemoryStream ms = new MemoryStream()) { Serializer.Serialize(ms, obj); Console.WriteLine(BitConverter.ToString(ms.ToArray())); } } } 

注意:我一些关于如何破解Google的连线格式以支持完整图表的理论,但是需要一些时间来尝试。 哦,重新“非常大的数组” – 对于原始types(不是对象),你可以使用“打包”编码为此; [DataMember(..., Options = MemberSerializationOptions.Packed)]可能是有用的,但很难说如果没有你的模型的可见性。

为什么不实现你自己的系统:: io:stream派生类? 这将允许您通过UpdateText直接将其附加到SQL列以进行编写。

例如(伪码)

插入数据库loggingblob列“初始化”(见上面的UpdateText文章)
创build您的streamtypes/关联与stream的数据库连接
将stream传递给序列化调用

它可以大块(每次8040字节的倍数,我假设)调用它,并在每个完整的缓冲区传递给DB UpdateText调用与适当的偏移量。

在streamclosures的时候,你可以刷新那些没有通过UpdateText完全填充缓冲区的内容。

同样,您可以使用相同的/类似的派生stream来读取数据库列,并将其传递给反序列化。

创build一个派生的stream并不是那么多的工作 – 我已经在C ++ / CLI中提供了与IStream的互操作性 – 如果我可以这样做:)…(我可以为您提供C ++ / CLIstream代码我如果这样做会有帮助,我们已经做了一个样本)

如果将整个操作(插入初始行,调用通过stream更新blob)插入到事务中,则在序列化步骤失败时,将避免任何潜在的db不一致。

我会去与文件。 基本上使用文件系统作为SQL Server和您的应用程序之间的中介。

  1. 在序列化大对象时,将其序列化成FileStream
  2. 将其导入数据库指示数据库在保存数据时直接使用该文件。 可能看起来像这样:

    INSERT INTO MyTable([MyColumn])SELECT b.BulkColumn,FROM OPENROWSET(BULK N'C:\ Path To My File \ File.ext',SINGLE_BLOB)as b

  3. 在读回数据时,指示SQL将大列作为临时文件保存到文件系统中,在将其反序列化到内存中之后(无需立即删除它,因为可能在此处执行caching),将删除它们。 不太确定这个sql命令是什么,因为我确定没有DB专家,但是我确信必须有一个。

  4. 再次使用FileStream对象将其反序列化回内存。

这个过程可以概括成一个助手类来完成,它会知道什么时候删除这些临时文件,如果你确实知道sql数据logging的值没有改变,你可以重用它们。

请注意,由于SQL Server 2012中还有FileTable,除了允许非事务性访问以外,与FILESTREAM类似。

https://msdn.microsoft.com/en-us/library/hh403405.aspx#CompareFileTable