Как сериализовать большой граф объекта .NET в большой двоичный объект SQL Server без создания большого буфера? - PullRequest
26 голосов
/ 20 января 2010

У нас есть код как:

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

Но поток памяти выделяет большой буфер из большой кучи памяти, которая доставляет нам проблемы. Итак, как мы можем передавать данные без необходимости свободной памяти для хранения сериализованных объектов.

Я ищу способ получить поток с сервера SQL, который затем можно передать в bin.Serialize (), чтобы избежать хранения всех данных в моей памяти процессов.

Аналогично для чтения данных обратно ...


Еще немного фона.

Это часть сложной системы числовой обработки, которая обрабатывает данные практически в реальном времени, выявляя проблемы с оборудованием и т. Д. Сериализация выполняется для перезапуска, когда возникает проблема с качеством данных из потока данных и т. Д. (Мы храним данные могут быть перезапущены после того, как оператор отредактировал неверные значения.)

Поэтому мы сериализуем объект гораздо чаще, чем десериализуем его.

Объекты, которые мы сериализуем, включают в себя очень большие массивы , в основном двойные, а также множество маленьких "более нормальных" объектов. Мы увеличиваем ограничение памяти в 32-битной системе и заставляем гаражный коллектор работать очень напряженно (Эффекты создаются в других местах системы, чтобы улучшить это, например, повторно использовать большие массивы, а не создавать новые массивы.)

Часто сериализация состояния - это последняя капля , которая обрабатывает исключение нехватки памяти; наше пиковое использование памяти - во время этой сериализации.

Я думаю, мы получаем большую фрагментацию пула памяти, когда мы десериализуем объект, я ожидаю, что есть и другая проблема с фрагментацией большого пула памяти, учитывая размер массивов. (Это еще не было исследовано, так как человек, который впервые посмотрел на него, является экспертом по числовой обработке, а не экспертом по управлению памятью.)

Если клиенты используют набор Sql Server 2000, 2005 и 2008, и мы бы не стали использовать разные пути кода для каждой версии Sql Server, если это возможно.

У нас может быть много активных моделей одновременно (в разных процессах, на многих машинах), каждая модель может иметь много сохраненных состояний. Следовательно, сохраненное состояние хранится в блобе базы данных, а не в файле.

Поскольку распространение сохранения состояния важно, я бы предпочел не сериализовать объект в файл, а затем помещать файл в BLOB по одному блоку за раз.

Другие связанные вопросы, которые я задал

Ответы [ 7 ]

37 голосов
/ 28 января 2010

Нет встроенной функции ADO.Net, которая бы действительно изящно справлялась с большими данными. Проблема в два раза:

  • отсутствует API для «записи» в команду (ы) SQL или параметры как в поток. Типы параметров, которые принимают поток (например, FileStream), принимают поток от него READ , что не согласуется с семантикой сериализации запись в поток. Независимо от того, каким образом вы это включите, вы получите в памяти копию всего сериализованного объекта, плохо.
  • , даже если указанная выше точка будет решена (а это не может быть), протокол TDS и способ, которым SQL Server принимает параметры, плохо работают с большими параметрами, поскольку весь запрос должен быть сначала получен до того, как он будет запущен в исполнение и это создаст дополнительные копии объекта внутри SQL Server.

Так что вам действительно нужно подойти к этому под другим углом. К счастью, есть довольно простое решение. Хитрость заключается в том, чтобы использовать высокоэффективный синтаксис UPDATE .WRITE и передавать порции данных один за другим в серии операторов T-SQL. Это рекомендуемый MSDN способ, см. Изменение больших (максимальных) данных в ADO.NET . Это выглядит сложно, но на самом деле тривиально сделать и подключить к классу Stream.


Класс BlobStream

Это хлеб с маслом решения. Производный класс 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

Чтобы использовать этот недавно созданный класс потока BLOB-объектов, вы подключаетесь к BufferedStream. Класс имеет тривиальный дизайн, который обрабатывает только запись потока в столбец таблицы. Я буду использовать таблицу из другого примера:

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, затем создадим BlobStream для вновь вставленного идентификатора и вызовем сериализацию прямо в этот поток:

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();
    }
}

Если вы проследите за выполнением этого простого примера, вы увидите, что нигде не создается большой поток сериализации. В примере будет выделен массив [1024 * 1024], но для демонстрационных целей есть что-то для сериализации. Этот код сериализуется буферизованным способом, порция за порцией, с использованием рекомендованного BLOB-объекта SQL Server размера обновления 8040 байт за раз.

11 голосов
/ 27 марта 2015

Все, что вам нужно, это .NET Framework 4.5 и потоковая передача.Предположим, у нас есть большой файл на жестком диске, и мы хотим загрузить этот файл.

Код 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 МБ, в то время как MemoryStream вызвала исключение при попытке загрузить этот файл в память.

UPD: этот код работает в Windows 7, но не работает в Windows XP и 2003 Server.

4 голосов
/ 24 января 2010

Как выглядит график?

Одна проблема здесь - поток; требование SQL 2005 - это боль, так как в противном случае вы могли бы писать напрямую в SqlFileStream, однако я не думаю, что было бы слишком сложно написать собственную реализацию Stream, которая буферизует 8040 (или несколько нескольких) байтов и записывает это постепенно. Тем не менее, я не уверен, что это стоит этой дополнительной сложности - я бы сильно испытал бы искушение просто использовать файл в качестве рабочего буфера и цикл затем (после сериализации) над вставка / добавление файлов. Я не думаю, что файловая система повредит вашей общей производительности здесь, и это спасет вас от начала обреченных данных - то есть вы не будете общаться с базой данных, пока не знаете, какие данные вы хотите записать. Это также поможет вам сократить время открытия соединения.

Следующая проблема - сама сериализация. Лично я не рекомендую использовать BinaryFormatter для записи в постоянные хранилища (только для транспорта), так как это зависит от реализации как в самом кодере, так и в ваших типах (т.е. это хрупко, если вы делаете невинно выглядящие изменения в ваших типах данных).

Если ваши данные могут быть в достаточной мере представлены в виде дерева (а не полного графика), я бы очень хотел попробовать буфер протокола / protobuf-net. Эта кодировка (разработанная Google) меньше, чем вывод BinaryFormatter, более быстрая как для чтения, так и для записи, и основана на контрактах, а не на полях, поэтому вы можете надежно повторить ее позже (даже если вы полностью переключите платформу) .

Параметры по умолчанию означают, что он должен записывать длину объекта перед каждым объектом (что может быть дорого в вашем случае), но если у вас есть вложенные списки больших (глубоких) объектов, вы можете использовать групповое кодирование, чтобы избежать этой необходимости - позволяя ему записывать поток только в прямом направлении, в один проход; Вот краткий простой пример использования групповой кодировки, но если вы хотите бросить в меня более сложный сценарий, просто дайте мне знать ...

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 для поддержки полных графиков, но потребуется некоторое время, чтобы попробовать. О, «очень большие массивы» - для примитивных типов (не объектов) вы можете использовать для этого «упакованную» кодировку; [DataMember(..., Options = MemberSerializationOptions.Packed)] - может быть полезным, но трудно сказать без видимости вашей модели.

4 голосов
/ 24 января 2010

Вы всегда можете писать в SQL Server на более низком уровне, используя протокол TDS (поток табличных данных), используемый Microsoft с самого первого дня. Они вряд ли изменят его в ближайшее время, так как даже SQLAzure использует его!

Вы можете увидеть исходный код того, как это работает, из проекта Mono и из проекта freetds

Проверьте tds_blob

2 голосов
/ 23 января 2010

Почему бы не реализовать свой собственный производный класс system :: io: stream? что позволит вам присоединить его к столбцу SQL напрямую через UpdateText для записи.

например (псевдокод)

Вставить запись в БД с колонкой BLOB-объектов 'initialized' (см. выше UpdateText статья)
Создайте свой тип потока / Связать соединение БД с поток
Передайте поток к сериализировать вызов

Он может распределять (по 8040 байтов за раз, я полагаю) вызовы к нему, и на каждом полном буфере передавать его в вызов DB UpdateText с соответствующим смещением.

При закрытии потока вы сбрасываете все, что осталось, что не полностью заполняет буфер через UpdateText.

Аналогично, вы можете использовать тот же / аналогичный производный поток, чтобы разрешить чтение из столбца БД, передавая его для десериализации.

Создание производного Stream - это не так уж много работы - я сделал это на C ++ / CLI, чтобы обеспечить совместимость с IStream - и если я могу это сделать:) ... (я могу предоставить вам C ++ / CLI код потока, который я сделал в качестве примера, если это будет полезно)

Если вы поместите всю транзакцию (вставка исходной строки, вызовы для обновления большого двоичного объекта через поток) в транзакцию, вы избежите любых потенциальных несоответствий БД, если шаг сериализации завершится неудачей.

1 голос
/ 13 апреля 2015

Обратите внимание, что, начиная с SQL Server 2012, существует также FileTable, аналогичный FILESTREAM, за исключением того, что он также позволяет осуществлять нетранзакционный доступ.

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

1 голос
/ 24 января 2010

Я бы пошел с файлами.В основном используйте файловую систему в качестве промежуточного звена между 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) как b

  3. При чтении данных, инструктируйте SQL сохранить большой столбец обратно в файловую систему как временный файл, который вы удалите после десериализации его в память (не нужно удалять его).немедленно, как можно кеширование можно сделать здесь).Не совсем уверен, что команда sql для этого, так как я точно не эксперт по БД, но я уверен, что он должен быть.

  4. Повторное использование FileStream объект для десериализации его обратно в память.

Эта процедура может быть обобщена для создания вспомогательного класса, который будет знать, когда удалять эти временные файлы, так как вы можете использовать их, еслиВы точно знаете, что значение записи данных sql не изменилось.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...