压缩持久化的 DataSet






4.30/5 (31投票s)
使用 .NET 2.0 的 DeflateStream 和 GZipStream 压缩持久化的 DataSets。
引言
使用 `WriteXML` 方法持久化缓存的 `DataSet` 会生成非常大的 XML 文件。.NET 2.0 引入了新的 `System.IO.Compression` 命名空间,它提供了用于压缩数据的流对象。当然,如果读写 XML 文件的性能有显著差异,那么压缩和解压缩就没有意义了。本文开头的图片显示了读写 XML 文件时有无压缩的性能差异。在本文开发过程中使用了 35 MB 的 XML 文件,但在示例代码中包含了一个较小的文件。本文的其余部分将重点介绍示例代码中的重要部分。您需要 Visual Studio 2005 Beta 2 才能构建示例,或者至少安装 .NET 2.0 可再发行组件包才能运行演示。
使用代码
作为基线,测量了将原始 XML 文件读入和写出 `DataSet` 所需的时间。
ts1 = New TimeSpan(Now.Ticks)
ds.ReadXml("..\input.xml")
ts2 = New TimeSpan(Now.Ticks)
Console.WriteLine("Took " & ts2.Subtract(ts1).ToString _
& " to read raw XML")
ts1 = New TimeSpan(Now.Ticks)
ds.WriteXml("test.xml")
ts2 = New TimeSpan(Now.Ticks)
Console.WriteLine("Took " & ts2.Subtract(ts1).ToString _
& " to write raw XML")
DeflateStream 类
根据 Microsoft 的文档
此类表示 Deflate 算法,这是一种行业标准的无损文件压缩和解压缩算法。它结合了 LZ77 算法和 Huffman 编码。通过仅使用先验边界的中间存储量,可以生成或消耗数据,即使是任意长的顺序输入数据流。该格式可以轻松实现,且不受专利限制。有关更多信息,请参阅 RFC 1951:DEFLATE 1.3 规范。
要将 `DataSet` 的 XML 写入压缩流,在调用 `WriteXml` 之前的唯一额外步骤是创建一个用于存储数据的文件,并创建 `DeflateStream` 对象;将文件的流传递进去,并将压缩模式设置为 `Compress`。
注意,如果 `DataSet` 很小,则由于初始头数据的开销,压缩后的 XML 文件可能会实际变得更大。
从文章开头的图片可以看出,压缩并写入输出流所需的时间仅比将原始 XML 写入文件慢大约 10-30%。然而,生成的文件要小大约 9 倍。
outfile = New FileStream("test.xmd", FileMode.Create, FileAccess.Write)
DefStream = New DeflateStream(outfile, CompressionMode.Compress, False)
ds.WriteXml(DefStream)
读取压缩的 XML 文件大致相同。在调用 `DataSet` 的 `ReadXml` 方法之前,以 `Read` 模式打开文件,并创建 `DeflateStream` 对象;将文件的流传递进去,并将压缩模式设置为 `Decompress`。解压缩并读取输入流所需的时间仅比从文件读取原始 XML 慢大约 10-30%。
infile = New FileStream("test.xmd", FileMode.Open, FileAccess.Read)
DefStream = New DeflateStream(infile, CompressionMode.Decompress, False)
ds.ReadXml(DefStream)
GZipStream 类
同样,根据 Microsoft 的文档
此类表示 gzip 数据格式,它使用行业标准的无损文件压缩和解压缩算法。该格式包含用于检测数据损坏的循环冗余校验值。gzip 使用与 `DeflateStream` 类相同的算法,但可以扩展以使用其他压缩格式。该格式可以轻松实现,且不受专利限制。gzip 的格式可参考 RFC 1952:GZIP 4.3 规范。
在前面的代码片段中,可以将 `DeflateStream` 替换为 `GZipStream`。有趣的是,`GZipStream` 类的性能略低于 `DeflateStream` 类。这可能是由于其可扩展性的额外开销。
BinaryFormatter 类 (更新)
基于最初文章的反馈,有人建议我查看使用 `BinaryFormatter` 类和 `DataSet` 的 `RemotingFormat` 属性存储的 `DataSet` 的大小和时间。由于 `DataSet` 通常在分布式应用程序的层之间传递,XML 会生成相当大的数据包。为了避免这种情况,Microsoft 提供了一种通过将 `DataSet` 的 `RemotingFormat` 属性设置为 `SerializationFormat.Binary` 来以二进制形式持久化 `DataSet` 的方法。
Dim formatter As New BinaryFormatter
ds.RemotingFormat = SerializationFormat.Binary
formatter.Serialize(outfile, ds)
从上图可以看出,二进制文件的大小约为原始 XML 文件大小的 1/3,但比压缩后的 XML 文件大大约 3 倍。然而,将二进制 `DataSet` 加载到内存中的时间比读取 XML 文件快了近 6 倍。出于兴趣,我决定压缩二进制文件,看看二进制数据是否能进一步压缩。
Dim formatter As New BinaryFormatter
ds.RemotingFormat = SerializationFormat.Binary
DefStream = New DeflateStream(outfile, CompressionMode.Compress, False)
formatter.Serialize(DefStream, ds)
结果是,压缩二进制数据所需的时间带来了大小上的微小减少,因此可能不值得使用。
关注点
我发现,在调用 `DataSet` 的 `WriteXml` 方法后,显式关闭压缩流和输出文件流非常重要。
outfile = New FileStream("test.xmz", FileMode.Create, FileAccess.Write)
ZipStream = New GZipStream(outfile, CompressionMode.Compress, False)
ds.WriteXml(ZipStream)
' neglecting to close either of the following streams
' results in a corrupted file when trying to read later
ZipStream.Close() ' important to close this first to flush compressed stream
outfile.Close() ' important to close this second to flush output stream
正如注释所示,如果这些流没有显式关闭,文件将损坏。稍后使用 `ReadXml` 读取压缩文件时将抛出“文件意外结束”异常。Microsoft 的文档中没有提到这一点,这可能是由于 beta 软件中的一个 bug。
运行演示后,尝试将生成的 `.xmd` 或 `.xmz` 文件重命名为 `.zip`。生成的压缩存档无法被 Windows 读取为有效的 Zip 文件。虽然有些人可能认为这是一个限制,但我认为这是一种简单的方式来保护 XML 数据的内容免受普通用户的侵害。
值得一提的是,对于那些寻求更灵活压缩格式的人来说,SharpZipLib 项目自 .NET 早期以来就已存在,并提供 Zip、GZip、Tar 和 BZip2 存档格式。
结论
新的 `GZipStream` 和 `DeflateStream` 类在付出很少额外成本的情况下,极大地减小了持久化到文件中的 `DataSet` 的大小。
将 `DataSet` 存储为二进制格式确实减小了持久化到文件中的 `DataSet` 的大小,但不如压缩方案效果好。然而,二进制文件加载速度要快得多。
我希望这篇文章能对您有所帮助。别忘了投票。
历史
- 2005-06-05 - 首次发布。
- 2005-06-14 - 更新了序列化为二进制格式。