65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 .NET (C#) 访问 SQL Server 虚拟设备接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (14投票s)

2007年7月3日

CPOL

10分钟阅读

viewsIcon

106720

downloadIcon

1121

本文介绍 SQL Server 虚拟设备接口及其如何通过任何 .NET 语言进行访问。

引言

SQL Server 的虚拟设备接口允许开发人员为 SQL Server 定义“虚拟设备”,用于备份和还原数据库及事务日志。访问这些虚拟设备可以让你访问在备份或还原操作期间进出 SQL Server 的原始数据。这种访问允许你对这些数据执行任意数量的任务,尽管最常见的任务是在数据写入磁盘之前对其进行压缩和/或加密。微软发布了一个 API 来访问 SQL Server 的这项功能。唯一的缺点是,据我所知,没有简单的方法可以通过 .NET Framework 访问此 API。因此,我着手开发了一个托管 C++ 组件,它将充当“虚拟设备接口”(VDI)API 和 .NET Framework 之间的桥梁。

背景

本文旨在介绍 VDI API 的基础知识以及如何将其集成到 .NET 应用程序中。因此,本文只涵盖了 VDI API 的一小部分。VDI 还有许多其他功能,例如多输入和输出流,以及各种性能调整机会。一旦你对这里概述的技术有了扎实的掌握,就可以 查看此参考资料 以获取有关 VDI 规范的更多详细信息。

Using the Code

总体方法

由于 VDI 的 API 是为 C/C++ 使用而设计的,因此使用托管 C++ 为 VDI 开发 .NET 包装器是合乎逻辑的。由于 VDI 的本质是向 SQL Server 流入和流出数据,因此使用 System.IO.Stream 进行 .NET 应用程序和 VDI 之间的通信对我来说是合理的。

接口

ExecuteCommand 方法

本文中的组件有一个名为 ExecuteCommand 的方法,顾名思义,它执行备份或还原命令到 VDI。此方法接受两个输入参数并返回 void。此方法的第一个参数是一个 string,表示一个正常的 SQL Server 备份或还原命令,但有一个要求。在指定将数据备份到的设备或从中还原数据的设备时,必须指定 VIRTUAL_DEVICE。此虚拟设备将在组件内命名,因此你需要使用 {} string 格式语法来指定实际设备名称。例如

BACKUP DATABASE AdventureWorks TO VIRTUAL_DEVICE='{0}' WITH STATS = 1

此功能的目的在于允许在发出 SQL Server 命令时最大程度地灵活。当然,在生产级别的应用程序中,你可能会限制此功能,但出于本文的教学目的,我将其保持开放。

此时我想指出,虽然上面的示例备份了一个数据库,但你也可以备份事务日志,执行差异备份,使用 restore verifyonly 验证备份,或者执行任何其他你通常通过 T-SQL 向 SQL Server 发出的命令。

该方法的第二个参数是任何派生自 System.IO.Stream 的对象。对于备份,这将是 SQL Server 写入的原始字节所在的流。最基本的使用方式是将 FileStream 对象作为此参数传递,该对象会将整个备份直接写入磁盘。生成的文件将与你通过查询分析器发出正常的 BACKUP DATABASE 命令一样。

另一个选择可能是将你的 FileStream 对象包装在 DeflateStream 对象中,并将 DeflateStream 对象作为参数传递。你最终将得到一个压缩的 SQL Server 备份。这里的优势在于,你无需先将整个备份写入磁盘然后再压缩该备份,而是可以直接写入压缩数据到磁盘,这样可以节省一个步骤、磁盘 I/O、时间和磁盘空间。

为了还原数据库,你只需反转过程,将包装在另一个 FileStream 对象(该对象从压缩文件中读取)周围的 DeflateStream 对象作为第二个参数,并附带适当的还原命令。

CommandIssued 事件

每当向组件发出命令时,都会触发此事件。如果你拦截此事件,你将看到正在向 VDI 发出的确切命令。

InfoMessageReceived 事件

每当 SQL Server 发送回信息性消息时,都会触发此事件。例如,在使用 WITH STATS 选项备份或还原数据库时,“处理 1%,处理 2%…” 的消息将实时显示,以便你监控长时间运行的操作。

内部

ExecuteCommand 方法

ExecuteCommand 方法首先为我们的使用设置和配置 VDI,并创建一个虚拟设备。VDI 最多支持 64 个虚拟设备。这意味着,在 .NET 端,你可以修改此组件以支持多达 64 个独立的流。如果你计划将输出写入多个磁盘,或者你想通过处理多个流(因此多个线程)的数据来利用多个处理器,这将很有用。为了简单起见,当前的 ExecuteCommand 方法仅支持一个设备和一个线程。

接下来,ExecuteCommand 方法格式化命令。如前所述,你传递给此方法的命令必须是“格式 string”格式,并指定 VIRTUAL_DEVICE 作为输出介质。此时,我们需要为虚拟设备生成一个唯一的名称。一个很好的机制是生成一个 GUID,这正是我们在本文中命名虚拟设备的方式。一旦我们将新创建的设备名称应用于命令,我们就会启动一个线程来执行该命令。

你可能想知道为什么我们不直接使用 SqlCommand 对象的异步 BeginExecuteNonQuery 方法来执行命令。你可以这样做,但 SqlConnection 对象的 InfoMessage 事件(处理来自 SQL Server 的所有消息)直到命令完成才会触发。当你希望在客户端应用程序中获得实时状态更新时,这是一个问题。解决方法是使用 OdbcCommand 在其自己的线程中执行,因为 OdbcConnection 对象的 InfoMessage 事件会立即触发。

在向 SQL Server 发出命令后,我们完成设备集的配置并打开我们的一个设备。

一旦发出命令并配置好设备,我们就会在 ExecuteCommand 方法中调用 ExecuteDataTransfer 方法。

ExecuteDataTransfer

一旦配置好设备并将命令发送到 SQL Server 执行备份或还原操作,ExecuteDataTransfer 方法就会处理其余部分。

有两个参数传递给此方法。第一个参数是我们之前在代码中创建的虚拟设备。第二个参数是 Stream 对象,它将包含还原所需的数据,或者接受备份传递给我们的数据。

此函数中的主循环会简单地调用虚拟设备的 GetCommand 方法,然后分析命令结构的 commandCode 属性以确定下一步的适当操作。

我们查找的第一个命令是 VDC_Read。这是我们在还原操作期间预期的命令。这是 SQL Server 对数据的请求。在此特定组件中,我们首先从 .NET 客户端应用程序传递的流中读取请求的字节数。我们将从流中读取的字节数存储在变量 bytesTransferred 中以供以后使用。然后,我们使用 System.Runtime.InteropServices.Marshal.Copy 方法将“.NET 字节”复制到 cmd->buffer 指定的内存位置。此时,我们确保 bytesTransferred 等于 cmd->size 参数。这确保了我们传输了 SQL Server 发送给我们的所有数据。如果我们匹配,我们就可以将 completionCode 设置为 ERROR_SUCCESS。如果出于某种原因,我们未能移动那么多字节,我们需要指定一个不同的完成代码来指示适当的错误情况。为了本练习的目的,如果我们无法读取适当数量的字节,我们将使用 ERROR_READ_FAULT completionCode

我们查找的下一个命令是 VDC_Write。这是我们在备份操作期间预期的命令。在这种特定情况下,我们首先将数据从 cmd->buffer 复制到 .NET 数组,然后将该 .NET 数组写入流。在这种情况下,我们不希望 bytesTransferred 是除 cmd->size 以外的任何值,因为如果写入流时出现问题,将引发异常。因此,我们将 bytesTransferred 参数始终设置为 cmd->size

其他命令包括 VDC_Flush,它只是一个提示我们刷新流的命令;VDC_ClearError 作用不大,但 VDI 规范要求;最后是一个默认情况,用于处理可能来自 VDI 的任何未知命令。

处理完传递给我们的命令后,我们调用虚拟设备的 CompleteCommand 方法。这会将反馈发送给 SQL Server,并告知它我们已准备好接收另一个命令。此循环将继续,直到 SQL Server 完成处理 T-SQL 命令(即发送到 SQL Server 的备份或还原命令),此时它将发出 VD_E_CLOSE HRESULT ,表示它已完成并正在关闭虚拟设备。此时,备份或还原操作完成。

关注点

访问 VDI 提供的最大机会之一是在将 SQL Server 备份写入磁盘之前在内存中压缩它们。只需将压缩文件流作为输入传递给 ExecuteCommand 方法,即可获得以下好处:

  1. 减少磁盘 I/O,因为写入磁盘的数据更少。
  2. 减少磁盘空间需求,因为备份已压缩。
  3. 如果你的压缩引擎足够快,则可能提高备份速度。
  4. 几乎可以肯定地提高还原速度,因为磁盘 I/O 较少,并且解压缩几乎总是比压缩快。

实现压缩的最简单方法是使用 .NET Framework 中的 DeflateStream GZipStream 对象。虽然有更快的压缩算法和实现,但这些是免费的,并且在压缩数据方面做得相当不错。

但是,使用这些类有两个注意事项。第一个是,如果你的数据库已经包含大量压缩数据(例如,图像文件、压缩二进制文件等),实际上可能会看到大小增加。这是因为 Deflate 算法的逐字实现(GZipStream 只是一个带有 CRC32 校验和的 Deflate 的包装器)。我不会详细介绍,但可以说其他 Deflate 实现会优化其代码以尽量减少这些影响。

第二个问题,它适用于 DeflateStream GZipStream 类,是文件大小。微软文档对这两个类有以下说法:

此类不能用于压缩大于 4 GB 的文件。

现在,话虽如此,我不知道他们指的是输入文件不能大于 4 GB,还是输出文件不能大于 4 GB。无论哪种情况,我都成功备份和还原了远大于 4 GB 的数据库。我使用至少 DeflateStream 创建的压缩备份文件也远大于 4 GB。我没有在 GZipStream 上测试过,但我预计结果会差不多。关键是,虽然这在我的测试中似乎有效,但微软说它无效。因此,如果你有大于 4 GB 的数据库/备份,你可能需要寻找备用的压缩流。

还有无数其他潜在的机会,例如:

你还可以使用 CryptoStream 对象轻松加密任何 SQL Server 备份,然后再将其写入磁盘,从而确保备份的未加密字节甚至不会命中磁盘。

你可以创建一个加密的 TCP 流,并在任何网络上直接备份数据库,而不必担心备份被拦截。

归根结底,你可以从任何 .NET 流进行备份和还原。这为任何语言的 .NET 开发人员提供了一个非常强大的工具来添加到他们的工具箱中。

历史

  • 2007 年 7 月 3 日:初始帖子
© . All rights reserved.