发布时间:2014-08-05 00:00 来源:未知
所有 SQL Server 数据访问应用程序编程接口 (API) 都提供了一个抽象来表示会话和会话中的请求。SQL Server 2000 以及更早的版本限制编程模型,它要求任何时候一个给定的会话中最多只能有一个待定的请求。有几个替代办法被用来解决这种限制,在这些替代办法中,最常见的可能就是服务器端光标。SQL Server 2005 实现了 Multiple Active Result Set (MARS),它解除了这个约束。该篇讲述了 MARS 的设计、结构和语义变更,以及为了从这些改进中得到最大收益,应用程序应当注意什么。
目前支持用于构建 SQL Server 应用程序的主要数据访问 API 是 ODBC、OLE-DB、ADO 和 SqlClient .Net Provider。1它们全部都提供一个抽象来表示一个指向服务器的已建立连接,同时提供另外一个抽象来表示在这个连接之下执行的请求。例如,SqlClient 使用 SqlConnection 和 SqlCommand 对象,而 ODBC 则使用 SQL_HANDLE_DBC 类型和 SQL_HANDLE_STMT 类型的句柄。
所有发送给 SQL Server 的执行请求都是几乎以下两种形式之一:1) 一组 T-SQL 语句,通常也称为批处理,或者 2) 存储过程或函数的名称,加上参数值(假如合适)。请注意,提交一个 SELECT 或 DML 语句给服务器是一个单语句批处理,这是第一类请求的特例。
在任何一种情况下,SQL Server 都会重申批处理或存储过程中包含的语句,然后执行这些语句。语句可能会生成结果,也可能不生成结果,并且语句可能会向调用者返回附加信息,也可能不返回。
结果主要是由 SELECT 和 FETCH 生成的。SQL Server 通过将结果返回给调用者来执行 SELECT 语句。这意味着,在查询执行引擎产生行的同时,这些行会被写入网络。更确切地说,所产生的这些行会被复制到预先保留的网络缓冲区中,然后缓冲区会被返回给调用者。网络写入操作会成功,并在客户端驱动程序从网络中读取时释放已用过的缓冲区。假如客户端没有消耗结果,在相同点上的网络写入操作将会被阻止,服务器中的网络缓冲区将会被填满,执行就会被挂起,等待状态和执行线程,直到客户端驱动程序捕获读取。这种产生结果和检索结果的模式通常被称为“默认结果集”,更正式的名称则是“流水游标”。
附加信息也可能以其他方式(可能没有结果返回方式那么明显)被返回给调用者。这种情况包括错误、警告和信息性消息。它们或者通过 PRINT 和 RAISERROR 语句显式返回,或者通过语句执行期间产生的警告和错误隐式返回。同样地,当 NOCOUNT 设置选项被设置为 OFF 时,SQL Server 会对每个已执行的语句发送一个“done row count”标记。这种附加信息也可能导致网络写入缓冲区被填满和执行被挂起。
这种背景使我们可以理解 SQL Server 2000 以及更早版本在支持每连接多个待定请求时的一些编程模型限制。
对于本文中的示例,我们将假定以下这样一个简单的情境:一个松散连接的清单处理系统使用“Operations”表作为队列来接收来自其他组件的请求:
OPERATIONS
已处理 |
位 |
|
operation_id |
int |
主键 |
operation_code |
char(1) |
'D' – decrease inventory 'I' – increase inventory 'R' – reserve inventory |
product_id |
uniqueidentifier |
|
数量 |
bigint |
我们假定这个组件管理着不同产品线和供应商的清单,而 product_id 则确定要使用哪个服务器和数据库以及如何执行请求的操作。(也就是说,假定不可能写入一些成组操作来处理队列中的所有请求)。
这个组件在一个插入到表中的松散处理请求中开始运行,并在成功完成指定操作时将这些请求标记为已处理。
伪代码类似如下所示:
while (1) { Get all messages currently available in Operations table; For each message retrieved { ProcessMessage(); Mark the message as processed; } }
前面一个使用 odbc 的方法类似如下所示(忽略了一些细节和错误处理):
SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1); SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt2); while (true) { SQLExecDirect(hstmt1, (SQLTCHAR*)"select operation_id, operation_code, product_id, quantity from dbo.operations where processed=0", SQL_NTS); while (SQL_ERROR!=SQLFetch(hstmt1)) { ProcessOperation(hstmt1); SQLPrepare(hstmt2, (SQLTCHAR*)"update dbo.operations set processed=1 where operation_id= ", SQL_NTS); SQLBindParameter(hstmt2, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &opid, 0, 0); SQLExecute(hstmt2); } }
但是,尝试执行 hstmt2 会导致:
[Microsoft][ODBC SQL Server Driver]Connection is busy with results for another hstmt.
使用 SqlClient 写入到 Microsoft Visual C# 中的相同逻辑类似如下所示:
SqlCommand cmd = conn.CreateCommand(); SqlCommand cmd2 = conn.CreateCommand(); cmd.CommandText= "select operation_id, operation_code, product_id, quantity from dbo.operations where processed=0"; cmd2.CommandText="update dbo.operations set processed=1 where operation_id=@operation_id"; SqlParameter opid=cmd2.Parameters.Add("@operation_id", SqlDbType.Int); reader=cmd.ExecuteReader(); while (reader.Read()) { ProcessOperation(); opid.Value=reader.GetInt32(0); // operation_id cmd2.ExecuteNonQuery(); }
同样地,尝试执行此逻辑会导致:
InvalidOperationException, There is already an open DataReader associated with this Connection which must be closed first.
这些错误是缺少 MARS 的最直接证明;在任何时候,一个给定的 SQL Server 连接之下最多只能有一个待定请求。
我有意忽略了 OLEDB,因为它会产生略微有点不同的行为。
由于以前版本的 SQLOLEDB 客户端驱动程序尝试模拟 MARS,因此在 MARS 方面应特殊对待 OLE-DB。然而,这种尝试具有很多缺陷。上述示例的一个 OLEDB 片断类似如下所示(还是那样,没有错误处理):
pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText); pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText2); pICommandText->SetCommandText(DBGUID_DBSQL, OLESTR("select operation_id, operation_code, product_id, quantity from dbo.operations where processed=0")); pICommandText2->SetCommandText(DBGUID_DBSQL,OLESTR("update dbo.operations set processed=1 where operation_id= ")); //Execute the command pICommandText->Execute(NULL, IID_IRowset, NULL, &cRowsAffected, (IUnknown**) &pIRowset); ... ProcessOperation(); ... //Execute the command 2 pICommandText2->Execute(NULL, IID_IRowset, NULL, &cRowsAffected, NULL);
真有趣,这段代码成功运行了,并且看来好像可以执行我想要执行的任务。假如说缺少 MARS 是一个基本引擎限制,它是怎么成功执行的?在经过一些检查之后,我们看到,SQLOLEDB 驱动程序在 covers 下面产生了一个新连接,并在其下执行 Command2。这意味着我已经有了 MARS,对吗?不完全准确。
数据库引擎中并没有特别执行什么任务,以便让这两个连接可以很好地协同工作。它们只是两个连接,因此它们具有不同的执行环境,并且更为重要的是,它们之间可能会产生冲突。SQLOLEDB 会防止在某个会话处于显式事务中时产生新连接,这是因为存在一个对 ITransactionLocal->StartTransaction 的显式调用,或是因为 DTC 事务中已登记了该会话。在这种情况下,执行命令 2 将会失败。
但是,假如其中一个命令通过 TSQL 开始一个事务,则 SQLOLEDB 就不会知道这种状况,并且会允许创建其他连接。不幸的是,两个不同的命令(表面上是相同会话的一部分)会在不同的事务下结束运行。
提升会话的孤立级别 - 比如说 REPEATABLE READ - 会使上述应用程序片断停留在一个不好的状态中。命令 1 运行查询,检索操作表中所有未处理的行。由于孤立级别较高,因此要等到事务结束时才会解除锁定。由于没有显式事务正在使用中,因此语句运行在 auto-commit 模式下,要等到语句结束时才会解除锁定。假如对某个特定行设置了锁定,而此时命令 2 修改该行,则会出现一个包含客户端代码的死锁,而应用程序会被挂起。
为了让结果变得更加难以预测,命令 1 中的语句将会在所生成的最后一行被复制到服务器的网络缓冲区中时完成,而不是在客户端应用程序读取最后一行时完成。这么做的含义是,对于较小的行集来说,上述代码片断将会成功执行,但假如数据卷太大,以至于服务器无法在执行命令 2 之前完成执行命令 1,则上述代码片断就会失败。一点也不奇怪,在开发环境中应用程序可以照常运行,但它在生产环境中部署时则会神奇地挂起。
从底线上说来,它可能不是可依赖 SQLOLEDB 的类似 MARS 行为的最佳应用程序设计。假如您决定使用它,请注意其他隐式连接以及这可能会带来的语义含义。
既然 SQL Server 2000 及更早版本中没有 MARS,那我怎么让我的应用程序工作?根据应用程序要求,有时需要显式使用多个连接。在很多情况下,使用服务器端光标会很方便。
服务器端光标为应用程序提供了一种使用一行或多个行的一个小段代码来消耗查询结果的方法。不再详细了解不同类型和选项,一般说来,一个光标就代表着查询结果。它维护着内存中的和磁盘上的状态,这样就可以根据需要返回查询结果。
在通常的使用模式下,会在声明指定查询之前先声明光标。一个提取操作会执行,以便从结果集中检索每一行或多个行。一旦这些行被消耗,或是不再需要结果集,光标就会被除去,从而释放出相关的服务器端资源。在本讨论中,要说明的最重要一点是,在两个提取操作之间没有代表光标执行任何代码。服务器上有保留状态,但是没有待定的工作。
ODBC 和 OLE-DB 会暴露属性,这样查询请求就会被映射为使用服务器端光标。
更改上述 ODBC 示例可以让第一个命令使用服务器端光标,从而使得应用程序情境成功执行并按照预期方式工作。一行更改:
SQLSetStmtAttr(hstmt1, SQL_ATTR_CURSOR_TYPE, (SQLPOINTER)SQL_CURSOR_DYNAMIC, 0);
类似的更改也可以应用于 OLE-DB,以帮助避免隐式产生连接和相关缺陷。
到此时为止,我们已经看到了服务器端光标是如何帮助解决缺少 MARS 的问题的。恰如我们将在后面介绍的,并不是说提供了 MARS 就不需要使用光标,也不是说所有使用光标的应用程序都必须改为使用 MARS。
自然而然,下一个问题就是“为什么不总是使用服务器端光标来替代看来限制更多的默认结果集”?1) 并不是所有有效的 SQL 上都支持所有光标类型,2) 光标一次只能操作一个 SELECT 语句以及 3) 性能。
由于结果在可用时被“推出”的方式,默认结果集会比服务器端光标执行得更好。另一方面,对于每次提取操作光标都会要求一个到服务器的往返。
总之,我们可以说 MARS 的所有目的就是为了解除“连接繁忙”,这对于编程模型来说是一个重大的改进。
SQL Server 2000 及更早版本中的会话可以处于以下任何一种可能的状态下:
• |
没有活动事务处于状态:这通常称为 auto-commit 模式,它意味着会话中执行的所有语句都运行在单独的事务中。 |
• |
本地事务处于活动状态:会话中执行的所有语句都运行在通过显式 TSQL BEGIN TRANSACTION 命令或 IMPLICIT_TRANSACTION ON 设置而启动的事务下。 |
• |
已登记:会话已在其他会话或其他事务管理器所拥有的事务中登记。前者是通过使用绑定会话 (sp_getbindtoken / sp_bindsession) 来实现,后者是通过在 DTC 事务中登记来实现的。 |
由于 MARS 不可用,因此在任何给定时间,在相同事务下不可能有多个语句正在执行。即使是在绑定会话或 DTC 情况下,基础结构也会确保在事务环境中,一个会话中一次只能发生一项工作。
在 ODBC 和 OLE-DB/ADO 中,在会话中启动事务之后,所有后续请求都会在可产生一个会话范围的事务环境这样的事务之下执行。
在 SqlClient 中,这个模型看来不太直观。我们提供了一个 API 以从连接 (SqlConnection) 对象开始一个事务,这个连接对象返回一个代表新创建事务的抽象 (SqlTransaction)。在一种看来比较随意的方式下,一旦事务被创建,假如没有将请求显式与事务上下文建立关联,就不能执行任何请求。SqlClient API 提供了一种编程模型,在这种编程模型下,事务不必在全局范围内作用于连接,多个事务可以在一个会话下创建,而请求可以被自由映射到任何活动事务。尽管 SQL Server 2005 不支持每个连接多个活动事务,但是编程模型已经可以适应这样一种未来的增强功能。
在 MARS 下,在一个给定的会话下可以有多个请求处于待定状态,从而,对于运行在相同事务下的请求之间发生的冲突要求适当的语义定义。
类似于 SQL Server 2000,请求对执行环境所做的任何更改都会成为全局会话更改。执行环境的确切含义是什么?它包含 SET 选项值(例如 ARITHABORT)、当前数据库上下文、执行状态变量(例如 @@error)、光标和临时表。
在申请更改当前数据库的请求中执行一个 USE 语句会导致所有后续请求都在新上下文中执行。类似地,更改批处理中 SET 选项的值则意味着所有后续执行都会在新设置值之下运行。
MARS 解除了一个会话下最多只能有一个待定请求的限制,在保留后向兼容性的同时,它为执行环境中的更改定义了更为精细的语义。
此时您对 MARS 是什么可能已经有了一个模糊的概念。简单说来,它就是能够让多个待定请求处于一个 SQL Server 连接之下的能力。多数情况下,这可以直接理解为在相同会话内部执行其他操作的同时存在多个默认结果集(流水游标)的能力。
分清 MARS 不是什么也很重要:
• |
平行执行:尽管 MARS 可以在一个连接之下提交多个请求,但这并不意味着这些请求会在服务器内平行执行。MARS 会在连接中的多个存在的请求之间复合执行线程,在预先定义好的点上交叉执行。 |
• |
光标替换:前面说过,在一些情况下光标可以作为缺少 MARS 的合适解决办法;它对于迁移这些情境使用 MARS 可能比较有效。但是,这并不意味着当前使用的所有光标都应当转为 MARS。 |
默认情况下,在对 SQL Server 2005 使用支持 MARS 的客户端驱动程序时,前面一节中包含的所有代码片断都“可以工作”。在支持 MARS 的连接下,上面所说的死锁情境(应用程序被挂起)现在也可以成功运行了。
支持 MARS 的客户端驱动程序如下:
• |
SQL Native Client 中包含的 SQLODBC 驱动程序 |
• |
SQL Native Client 中包含的 SQLOLEDB 驱动程序 |
• |
.NET Framework 版本 2.0 中包含的 SqlClient .Net Data Provider |
默认情况下,这些驱动程序将会建立支持 MARS 的连接。假如出于某种原因,您想要建立保留下级驱动程序行为的连接,每个 API 都提供了一个请求非 MARS 连接的选项。
SqlClient 提供了 MultipleActiveResultSets 连接字符串选项。假如此项设置为 false,则不允许对会话使用 MARS。假如此项设置为 true 或忽略此项,则可以使用 MARS。
同样地,ODBC 提供了一个 SQL_COPT_SS_MARS_ENABLED 连接选项,而 OLE-DB 提供了一个 SSPROP_INIT_MARSCONNECTION 选项。再强调一次,仅当需要禁用 MARS 时(因为默认是启用 MARS 的)才需要使用这些选项。
注意只有 SQL Native Client 版的 ODBC 和 OLEDB 提供程序才提供 MARS。旧版的提供程序还没有增强到支持 MARS。不用说,新版驱动程序在连接到 SQL Server 2000 或 更早版本的服务器时也不支持 MARS。
在更深层含义上,MARS 是关于在一个连接内启用多个请求的交叉执行。也就是说,允许成批执行,并且在执行中还允许执行其他请求。请注意,MARS 是在交叉执行而不是平行执行的意义上定义的。
MARS 结构允许多个批处理以交叉方式执行,尽管执行只能在定义良好的点切换。事实上,大多数语句都必须运行在一个批处理中。只有以下语句可以在完成之前交叉执行:
• |
SELECT |
• |
FETCH |
• |
READTEXT |
• |
RECEIVE |
• |
BULK INSERT(或 bcp 接口) |
• |
异步光标填充 |
它的确切含义是什么?它的含义是必须等到作为存储过程或批处理的一部分执行的不在此列表中的任何其他语句运行完毕,执行才可以被切换到其他 MARS。
作为示例,可以设想有一个提交了较长的运行中 DML 语句的批处理;假定就是一个 UPDATE 语句(它会影响几十万个记录)。假如正在执行这个语句时,提交了第二个批处理,那么它的执行就要等到 UPDATE 语句完成时才会开始。
另一方面,假如 SELECT 语句是第一个提交的语句,那么 UPDATE 语句就可以在 SELECT 语句中运行。但是,在 DML 操作完成之前,不会为 SELECT 语句生成任何新行。
这再一次说明了 MARS 交叉处理而不是平行处理请求。交叉处理不会受到请求语句位置的影响,无论它是位于批处理中、位于 EXEC 语句中还是位于存储过程中。
注意只要开始生成行,RECEIVE 语句就可以交叉处理。对于在 WAITFOR 子句中执行的 RECEIVE 语句,当该语句处于等待状态时,它是不可交叉处理的。
注意只有在 SET XACT_ABORT ON 之下执行,并且插入操作的表目标中没有定义任何触发器,或者指定了不引发触发器的选项,Bulk 操作才可以交叉执行。RECEIVE 只有在 XACT_ABORT 设置为 ON 是才可以交叉执行。
注意执行托管代码时,无法交叉执行用任何 .Net 语言编写的存储过程。假如使用了 inproc 数据提供程序,那么已经执行的批处理就要受到语句的交叉执行和原子执行的普通规则约束。
上面说过,MARS 是 SQL Server 数据访问 API 的默认处理模式。它的执行模式 – 不像服务器端光标 – 支持大批量语句,并且还可以调用存储过程和动态 sql 操作。对于在其中生成结果的“流水游标”模式,默认结果集 (MARS) 的性能会优于服务器端光标。
尽管其中还有某种很好的打印。默认结果集会“尽可能快地”生成结果。只要客户端驱动程序或应用程序正在消耗所生成的结果,就会产生这种情况。假如应用程序没有消耗结果,服务器端缓冲区会被填满,处理就会被挂起,直到结果被消耗。当执行被挂起时,很多资源会被占用:数据和架构被锁定,服务器工作线程被占用(包括堆栈和其他相关内存)。请注意,这种情况不仅限于 MARS;当一个请求生成了没有被快速消耗的默认结果集时,它代表 SQL Server 2000 和更早版本所导致的开销是相同的。MARS 并不意味着在流水游标开销方面的改进。
对于服务器端光标,并不存在这种资源占用的情况。在某种程度上有些关系,根据请求的光标类型,可能可以使用默认结果集无法使用的其他语义,即结果的可滚动性和可更新性。
由于上面已经介绍了请求的处理过程,现在我们直接推断从 SQL Server(假定不要求可滚动性和可更新性)检索结果的用法指南:假如应用程序能够快速地消耗结果,那么 MARS 下的默认结果集就可以提供最好的性能和开销特点。假如应用程序消耗结果的速度比较缓慢,则建议使用服务器端光标,尤其是 FAST_FORWARD 光标。
在大多数情况下,适合使用 MARS 默认结果集。那么有什么缓慢消耗结果的示例呢?想像一下执行返回结果的批处理或存储过程的应用程序,行的消耗取决于数据库外部操作(例如用户输入、UI 操作、与其他任务同步,等等)的完成。一般说来,请求长时间处于待定状态会影响应用程序和 SQL Server 的可伸缩性。
MARS 的引入改变了数据库引擎内部很多现有的假设,包括事务语义和一个事务内的并发操作。
尽管 OLE-DB 在会话中有一个事务处于活动状态时会禁止隐式产生连接,并且 ODBC 也经常使用“连接繁忙”来拒绝其他请求,但在支持 MARS 的世界里,这些事情都可以成功做到。假如会话中有一个活动事务,所有新的请求都会运行在指定事务之下;假如会话中没有活动事务,那么每个批处理都会运行在 auto commit 模式下,这意味着每个执行的语句都运行在它自己的事务之下。
SqlClient 托管提供程序的模型更为明显。Specific SqlCommands 需要关联给定的 SqlTransaction 对象,以便指定特定请求运行在哪个事务之下。
一般说来,事务确定了多个用户之间的隔离。但是在 MARS 下,多个请求可以运行在相同的事务之下,这使得请求之间彼此兼容,并避免产生“重新捕获”一节中所述的死锁。但是,假如在相同事务下有两个请求之间存在冲突操作,又会怎么样呢?
有几种可能的情况,下面分别说明:
• |
一个请求正在读取某些结果(比如说 SELECT、FETCH 或 READTEXT)。另外一个请求修改了正在读取的数据(比如说 DML 操作)。在这种情况下,尽管更改操作成功,但读取操作独立于更改,因此所有读取的数据看来就是读取操作开始时的状态。请注意,只有读取操作先于修改操作开始,才有可能产生这种情况。假如 DML 语句首先开始执行,那么读取操作就要等到后面才执行,这样就可以看到所有更改了。 |
• |
两个请求尝试修改相同数据。对于语句的原子数规则,必须在 DML 语句运行完毕后才能运行其他语句。因此,尝试修改数据的两个批处理永远不会交叉执行。请求会按照顺序执行,而结果会反应执行的顺序。请注意,假如客户端应用程序是多线程的,这可能会产生非确定性行为。 |
• |
一个请求正在读取数据(比如说 SELECT、FETCH 或 READTEXT),但任一基本对象的架构被修改了(比如说 DML 操作)。在这种情况下,DDL 操作会失败,因为在相同的事务下存在冲突的待定请求。请注意,这种行为也适用于在 RECEIVE 语句产生结果时尝试更改服务代理队列架构的情况。 |
• |
重叠操作发生在被批量插入的表上。BULK INSERT(或 bcp、IRowsetFastLoad)可以非原子运行,即可以与其他语句交叉执行。但是,在 BULK INSERT 的对象目标上不能并发执行任何 DDL、DML 或读取操作。这种情况下会产生一个错误,因为在相同的事务下存在冲突的待定请求。 |
请记住,上述情况仅适用于请求运行在相同事务下。对于运行在不同事务下的请求,则适用常规的锁定、阻止或隔离语义。
顺便说一下,事务框架实现的 MARS 下的事务语义现在也用于绑定会话和 DTC。这意味着尽管它以前只能在没有请求处于待定状态时更改事务上下文,现在则能够在支持非原子运行的一组相同语句期间切换上下文。同样地,在 DML、DDL 和其他必须原子运行的语句执行时,不能切换事务上下文。
注意假如在给定事务下存在待定的请求,则尝试提交事务将会失败。
事务保存点通常用于允许在一个事务内部部分回滚。通常,应用程序开始一个事务,设置保存点,进行某些工作,然后假如工作成功则继续,不成功的话则回滚到保存点。以下示例显式了保存点运行在相同事务下两个事务的交叉:
时间 | 批处理 1 | 批处理 2 |
T1 |
开始事务; |
|
T2 |
删除 dbo.operations,其中 operation_id=5; |
|
T3 |
保存事务 sp1; | |
T4 |
插入 dbo.operations 默认值; | |
T5 |
删除 dbo.operations,其中 operation_id=10; |
|
T6 |
插入 dbo.operations 默认值; | |
T7 |
假如 @@error>0 回滚事务 sp1; | |
... |
||
Tn |
提交事务; |
在上述示例中,第一个请求开始一个事务,并执行某项工作(删除一行)。然后批处理 2 开始运行(在相同事务下),并尝试设置保存点,以确保事务内的一组给定的语句要么成功执行,要么原子失败。
但是,在批处理 2 执行这两个语句的时候,一个来自批处理 1 的删除操作被交叉执行。假定批处理 2 中出现了一个错误,请求将会尝试回滚到保存点 sp1。但是,这也会“静态”回滚批处理 1 在 T5 时执行的删除操作。
为了避免这种不可预测和难以调试的情况发生,当一个事务中有多个活动请求正在运行时,MARS 不允许设置保存点、回滚到保存点和提交事务。假如上述这两个请求按照顺序执行,操作会成功,但在指定的并发请求如上述方式交叉执行的情况下,在批处理 2 中设置保存点的尝试将会失败,并产生一个错误。
前面说过,看起来执行环境好像是一个遍布整个会话的全局环境。在 MARS 下,假如多个请求同时更改环境会发生什么?请看如下示例:
时间 | 批处理 1 | 批处理 2 | 批处理 3 |
T1 |
使用 operations; |
||
T2 |
使用 msdb; |
||
T3 |
从 dbo.operations 中选择 operation_id; |
从 sys.objects 中选择名称; |
|
... |
|||
Tn |
从 sys.objects 中选择名称; |
上面的示例显式了三个批处理运行在相同的连接下。批处理 1 和批处理 2 更改数据库上下文,然后运行 SELECT 语句。批处理 3 则在稍后的时间运行 SELECT 语句,但没有指定数据库上下文。假如执行环境是连接的真正全局状态,那么上述组合的结果对于应用程序开发来说将是令人困惑和不可预测的。
MARS 具有请求级别执行环境和会话级别默认执行环境。当请求开始执行时,会话级别环境就会被克隆成请求级别环境。在整个批处理完成之后,所产生的环境会被复制回会话级别默认执行环境。
在这种语义下,请求的执行顺序(SQL Server 2000 中唯一允许的行为)提供了一个单一会话全局执行环境的假象。但是,在并发 MARS 请求下,一个请求所做的更改并不会影响其他并发执行的请求。
在上述示例中,会话环境被复制到批处理 1 和批处理 2 上,然后这两个批处理的 SELECT 语句就会运行在所期望的数据库上下文中。完成之后,这两个批处理就会复制(和覆盖)会话环境。请注意,在在这种情况下,所产生的会话数据库将取决于批处理 1 和 2 的完成时间。假如批处理 3 在批处理 1 和批处理 2 完成之后开始执行,那么根据前面两个请求的时间,返回的结果将对应于“operations”或“msdb”数据库。
请注意在 MARS 下编写多个批处理程序的上下文复制语义。上下文的复制包括 SET 选项和其余的执行环境。
注意假如某个批处理被取消,执行环境默认会被复制回取消请求被确认时的会话环境。
MARS 支持几种新的情境,但由于这通常是一些具有强大功能的情况,因此它也增加了您射死自己脚步的机会。请看以下示例。
传统上,允许对 DML 语句使用触发器。SQL Server 2005 扩展了这种模型,它允许在 DDL 语句上定义触发器。现在,我们来看看应用程序决定向调用方放回来自触发器内部的结果(当然不是您将要按照这个可怕的练习操作的)。触发器主体定义中包含一个 SELECT 语句。从应用程序角度看来,伪代码应当类似如下所示:
Request 1: update table operations; // this will return results from the trigger. For each row returned from the trigger { Request 2: Read from some other table based on current row; }
利用上述示例,我们创建了一种新型的应用程序死锁。从 request 1 每返回一行,Request 2 都会尝试执行。但是,由于 request 1 是一个 DML 语句,因此必须等它运行完毕,任何其他语句才可以执行。相同情况也适用于 DDL 语句。在这种情况下,要等到 request 1 完成后 request 2 才会运行。但是,request 1 的完成又取决于每个 request 2 的执行。
MARS 通过在死锁检测链中添加阻止网络操作,从而解决了这个问题。对于上述情境,SQL Server 的死锁监视器将会检测环境,然后终止 Request 2 并显示一条错误信息,指出会话正忙于响应其他请求。
作为一般性规则,请记住哪些语句必须原子运行,并确保没有任何操作会阻止它们前进。更重要的是,确保它们不会被依赖于前面语句完成的操作所阻止。
从触发器返回结果是在这种环境中运行的最简单的方式。由于这个原因和其他一些原因,我们强烈建议不要从触发器返回结果。这包括没有分配子句的 SELECT 语句、没有分配子句的 FETCH 语句、PRINT 语句以及其他在 NOCOUNT 设置为 OFF 时运行的语句。
恰如我们看到的,MARS 已经改变了 SQL Server 引擎内的一些核心假设。很重要的一点是,在监视和诊断 SQL Server 实例时请记住一些新假设。
在 SQL Server 中,一个 spid 代表一个会话。由于以前版本中没有 MARS,因此很普遍地就将 spid 与请求关联在一起。普遍想到要检索某个给定 spid 的 SQL 文本。普遍在多个系统过程中查找 spid 的执行统计。所有这些对于支持 MARS 的情境来说可能不再足够。
尽管系统进程继续显示某个会话的信息,我们还是实现了一些改进以帮助监视 MARS。
新的“动态管理视图”(DMV) sys.dm_exec_sessions 代表了会话信息的新视图,包括会话默认执行环境。在这种视图下,传统上的 spid 反映在 session_id 列下。
同时,sys.dm_exec_connections 也可用于显示所有已经建立的指向服务器的物理和逻辑连接。逻辑连接是为运行在 MARS 下的每个请求建立的会话中的虚拟管道。对于逻辑连接来说,parent_connection_id 列是被填充的。普通 session_id 列也显示了一个会话中多个逻辑连接的关系。
一个新的 DMV(sys.dm_exec_requests)表示每个会话下可用请求的详细列表。
还引入了一个新的内部函数 current_request_id(),以便可以通过编程方式找到当前执行请求的 id。.这类似于现有的 @@spid 函数。
SQL Server 2005 中的 Multiple Active Result Sets (MARS) 支持增加了开发 SQL Server 应用程序的选项。它带来了光标编程模型与相关引擎的默认处理模型的性能和功能。
MARS 为使用多个连接来克服缺少 MARS 问题的一些应用程序提供了很好的替代办法。但是,有时也不一定使用 MARS,因为多个连接确实在服务器中提供了平行执行的能力(假如它们没有在相同事务中登记的话)。
尽管在很多情况下,MARS 可以作为服务器端光标的替代办法,并可以提供一些性能改进,但它并不能替代光标。恰如本白皮书中介绍的,在很多情况下 MARS 是很好的替代办法,但也有很多情况使用光标会更为合适。
简单说来,MARS 是一个编程模型增强,它允许多个请求可以在服务器中交叉执行。尽管它不代表可以在服务器中平行执行,但假如正确使用的话它确实可以产生一些性能优点。
所有 SQL Server 数据访问应用程序编程接口 (API) 都提供了一个抽象来表示会话和会话中的请求。SQL Server 2000 以及更早的版本限制编程模型,它要求任何时候一个给定的会话中最多只能有一个待定的请求。有几个替代办法被用来解决这种限制,在这些替代办法中,最常见的可能就是服务器端光标。SQL Server 2005 实现了 Multiple Active Result Set (MARS),它解除了这个约束。该篇讲述了 MARS 的设计、结构和语义变更,以及为了从这些改进中得到最大收益,应用程序应当注意什么。
目前支持用于构建 SQL Server 应用程序的主要数据访问 API 是 ODBC、OLE-DB、ADO 和 SqlClient .Net Provider。1它们全部都提供一个抽象来表示一个指向服务器的已建立连接,同时提供另外一个抽象来表示在这个连接之下执行的请求。例如,SqlClient 使用 SqlConnection 和 SqlCommand 对象,而 ODBC 则使用 SQL_HANDLE_DBC 类型和 SQL_HANDLE_STMT 类型的句柄。
所有发送给 SQL Server 的执行请求都是几乎以下两种形式之一:1) 一组 T-SQL 语句,通常也称为批处理,或者 2) 存储过程或函数的名称,加上参数值(假如合适)。请注意,提交一个 SELECT 或 DML 语句给服务器是一个单语句批处理,这是第一类请求的特例。
在任何一种情况下,SQL Server 都会重申批处理或存储过程中包含的语句,然后执行这些语句。语句可能会生成结果,也可能不生成结果,并且语句可能会向调用者返回附加信息,也可能不返回。
结果主要是由 SELECT 和 FETCH 生成的。SQL Server 通过将结果返回给调用者来执行 SELECT 语句。这意味着,在查询执行引擎产生行的同时,这些行会被写入网络。更确切地说,所产生的这些行会被复制到预先保留的网络缓冲区中,然后缓冲区会被返回给调用者。网络写入操作会成功,并在客户端驱动程序从网络中读取时释放已用过的缓冲区。假如客户端没有消耗结果,在相同点上的网络写入操作将会被阻止,服务器中的网络缓冲区将会被填满,执行就会被挂起,等待状态和执行线程,直到客户端驱动程序捕获读取。这种产生结果和检索结果的模式通常被称为“默认结果集”,更正式的名称则是“流水游标”。
附加信息也可能以其他方式(可能没有结果返回方式那么明显)被返回给调用者。这种情况包括错误、警告和信息性消息。它们或者通过 PRINT 和 RAISERROR 语句显式返回,或者通过语句执行期间产生的警告和错误隐式返回。同样地,当 NOCOUNT 设置选项被设置为 OFF 时,SQL Server 会对每个已执行的语句发送一个“done row count”标记。这种附加信息也可能导致网络写入缓冲区被填满和执行被挂起。
这种背景使我们可以理解 SQL Server 2000 以及更早版本在支持每连接多个待定请求时的一些编程模型限制。
对于本文中的示例,我们将假定以下这样一个简单的情境:一个松散连接的清单处理系统使用“Operations”表作为队列来接收来自其他组件的请求:
OPERATIONS
已处理 |
位 |
|
operation_id |
int |
主键 |
operation_code |
char(1) |
'D' – decrease inventory 'I' – increase inventory 'R' – reserve inventory |
product_id |
uniqueidentifier |
|
数量 |
bigint |
我们假定这个组件管理着不同产品线和供应商的清单,而 product_id 则确定要使用哪个服务器和数据库以及如何执行请求的操作。(也就是说,假定不可能写入一些成组操作来处理队列中的所有请求)。
这个组件在一个插入到表中的松散处理请求中开始运行,并在成功完成指定操作时将这些请求标记为已处理。
伪代码类似如下所示:
while (1) { Get all messages currently available in Operations table; For each message retrieved { ProcessMessage(); Mark the message as processed; } }
前面一个使用 odbc 的方法类似如下所示(忽略了一些细节和错误处理):
SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1); SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt2); while (true) { SQLExecDirect(hstmt1, (SQLTCHAR*)"select operation_id, operation_code, product_id, quantity from dbo.operations where processed=0", SQL_NTS); while (SQL_ERROR!=SQLFetch(hstmt1)) { ProcessOperation(hstmt1); SQLPrepare(hstmt2, (SQLTCHAR*)"update dbo.operations set processed=1 where operation_id= ", SQL_NTS); SQLBindParameter(hstmt2, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &opid, 0, 0); SQLExecute(hstmt2); } }
但是,尝试执行 hstmt2 会导致:
[Microsoft][ODBC SQL Server Driver]Connection is busy with results for another hstmt.
使用 SqlClient 写入到 Microsoft Visual C# 中的相同逻辑类似如下所示:
SqlCommand cmd = conn.CreateCommand(); SqlCommand cmd2 = conn.CreateCommand(); cmd.CommandText= "select operation_id, operation_code, product_id, quantity from dbo.operations where processed=0"; cmd2.CommandText="update dbo.operations set processed=1 where operation_id=@operation_id"; SqlParameter opid=cmd2.Parameters.Add("@operation_id", SqlDbType.Int); reader=cmd.ExecuteReader(); while (reader.Read()) { ProcessOperation(); opid.Value=reader.GetInt32(0); // operation_id cmd2.ExecuteNonQuery(); }
同样地,尝试执行此逻辑会导致:
InvalidOperationException, There is already an open DataReader associated with this Connection which must be closed first.
这些错误是缺少 MARS 的最直接证明;在任何时候,一个给定的 SQL Server 连接之下最多只能有一个待定请求。
我有意忽略了 OLEDB,因为它会产生略微有点不同的行为。
由于以前版本的 SQLOLEDB 客户端驱动程序尝试模拟 MARS,因此在 MARS 方面应特殊对待 OLE-DB。然而,这种尝试具有很多缺陷。上述示例的一个 OLEDB 片断类似如下所示(还是那样,没有错误处理):
pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText); pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText2); pICommandText->SetCommandText(DBGUID_DBSQL, OLESTR("select operation_id, operation_code, product_id, quantity from dbo.operations where processed=0")); pICommandText2->SetCommandText(DBGUID_DBSQL,OLESTR("update dbo.operations set processed=1 where operation_id= ")); //Execute the command pICommandText->Execute(NULL, IID_IRowset, NULL, &cRowsAffected, (IUnknown**) &pIRowset); ... ProcessOperation(); ... //Execute the command 2 pICommandText2->Execute(NULL, IID_IRowset, NULL, &cRowsAffected, NULL);
真有趣,这段代码成功运行了,并且看来好像可以执行我想要执行的任务。假如说缺少 MARS 是一个基本引擎限制,它是怎么成功执行的?在经过一些检查之后,我们看到,SQLOLEDB 驱动程序在 covers 下面产生了一个新连接,并在其下执行 Command2。这意味着我已经有了 MARS,对吗?不完全准确。
数据库引擎中并没有特别执行什么任务,以便让这两个连接可以很好地协同工作。它们只是两个连接,因此它们具有不同的执行环境,并且更为重要的是,它们之间可能会产生冲突。SQLOLEDB 会防止在某个会话处于显式事务中时产生新连接,这是因为存在一个对 ITransactionLocal->StartTransaction 的显式调用,或是因为 DTC 事务中已登记了该会话。在这种情况下,执行命令 2 将会失败。
但是,假如其中一个命令通过 TSQL 开始一个事务,则 SQLOLEDB 就不会知道这种状况,并且会允许创建其他连接。不幸的是,两个不同的命令(表面上是相同会话的一部分)会在不同的事务下结束运行。
提升会话的孤立级别 - 比如说 REPEATABLE READ - 会使上述应用程序片断停留在一个不好的状态中。命令 1 运行查询,检索操作表中所有未处理的行。由于孤立级别较高,因此要等到事务结束时才会解除锁定。由于没有显式事务正在使用中,因此语句运行在 auto-commit 模式下,要等到语句结束时才会解除锁定。假如对某个特定行设置了锁定,而此时命令 2 修改该行,则会出现一个包含客户端代码的死锁,而应用程序会被挂起。
为了让结果变得更加难以预测,命令 1 中的语句将会在所生成的最后一行被复制到服务器的网络缓冲区中时完成,而不是在客户端应用程序读取最后一行时完成。这么做的含义是,对于较小的行集来说,上述代码片断将会成功执行,但假如数据卷太大,以至于服务器无法在执行命令 2 之前完成执行命令 1,则上述代码片断就会失败。一点也不奇怪,在开发环境中应用程序可以照常运行,但它在生产环境中部署时则会神奇地挂起。
从底线上说来,它可能不是可依赖 SQLOLEDB 的类似 MARS 行为的最佳应用程序设计。假如您决定使用它,请注意其他隐式连接以及这可能会带来的语义含义。
既然 SQL Server 2000 及更早版本中没有 MARS,那我怎么让我的应用程序工作?根据应用程序要求,有时需要显式使用多个连接。在很多情况下,使用服务器端光标会很方便。
服务器端光标为应用程序提供了一种使用一行或多个行的一个小段代码来消耗查询结果的方法。不再详细了解不同类型和选项,一般说来,一个光标就代表着查询结果。它维护着内存中的和磁盘上的状态,这样就可以根据需要返回查询结果。
在通常的使用模式下,会在声明指定查询之前先声明光标。一个提取操作会执行,以便从结果集中检索每一行或多个行。一旦这些行被消耗,或是不再需要结果集,光标就会被除去,从而释放出相关的服务器端资源。在本讨论中,要说明的最重要一点是,在两个提取操作之间没有代表光标执行任何代码。服务器上有保留状态,但是没有待定的工作。
ODBC 和 OLE-DB 会暴露属性,这样查询请求就会被映射为使用服务器端光标。
更改上述 ODBC 示例可以让第一个命令使用服务器端光标,从而使得应用程序情境成功执行并按照预期方式工作。一行更改:
SQLSetStmtAttr(hstmt1, SQL_ATTR_CURSOR_TYPE, (SQLPOINTER)SQL_CURSOR_DYNAMIC, 0);
类似的更改也可以应用于 OLE-DB,以帮助避免隐式产生连接和相关缺陷。
到此时为止,我们已经看到了服务器端光标是如何帮助解决缺少 MARS 的问题的。恰如我们将在后面介绍的,并不是说提供了 MARS 就不需要使用光标,也不是说所有使用光标的应用程序都必须改为使用 MARS。
自然而然,下一个问题就是“为什么不总是使用服务器端光标来替代看来限制更多的默认结果集”?1) 并不是所有有效的 SQL 上都支持所有光标类型,2) 光标一次只能操作一个 SELECT 语句以及 3) 性能。
由于结果在可用时被“推出”的方式,默认结果集会比服务器端光标执行得更好。另一方面,对于每次提取操作光标都会要求一个到服务器的往返。
总之,我们可以说 MARS 的所有目的就是为了解除“连接繁忙”,这对于编程模型来说是一个重大的改进。
SQL Server 2000 及更早版本中的会话可以处于以下任何一种可能的状态下:
• |
没有活动事务处于状态:这通常称为 auto-commit 模式,它意味着会话中执行的所有语句都运行在单独的事务中。 |
• |
本地事务处于活动状态:会话中执行的所有语句都运行在通过显式 TSQL BEGIN TRANSACTION 命令或 IMPLICIT_TRANSACTION ON 设置而启动的事务下。 |
• |
已登记:会话已在其他会话或其他事务管理器所拥有的事务中登记。前者是通过使用绑定会话 (sp_getbindtoken / sp_bindsession) 来实现,后者是通过在 DTC 事务中登记来实现的。 |
由于 MARS 不可用,因此在任何给定时间,在相同事务下不可能有多个语句正在执行。即使是在绑定会话或 DTC 情况下,基础结构也会确保在事务环境中,一个会话中一次只能发生一项工作。
在 ODBC 和 OLE-DB/ADO 中,在会话中启动事务之后,所有后续请求都会在可产生一个会话范围的事务环境这样的事务之下执行。
在 SqlClient 中,这个模型看来不太直观。我们提供了一个 API 以从连接 (SqlConnection) 对象开始一个事务,这个连接对象返回一个代表新创建事务的抽象 (SqlTransaction)。在一种看来比较随意的方式下,一旦事务被创建,假如没有将请求显式与事务上下文建立关联,就不能执行任何请求。SqlClient API 提供了一种编程模型,在这种编程模型下,事务不必在全局范围内作用于连接,多个事务可以在一个会话下创建,而请求可以被自由映射到任何活动事务。尽管 SQL Server 2005 不支持每个连接多个活动事务,但是编程模型已经可以适应这样一种未来的增强功能。
在 MARS 下,在一个给定的会话下可以有多个请求处于待定状态,从而,对于运行在相同事务下的请求之间发生的冲突要求适当的语义定义。
类似于 SQL Server 2000,请求对执行环境所做的任何更改都会成为全局会话更改。执行环境的确切含义是什么?它包含 SET 选项值(例如 ARITHABORT)、当前数据库上下文、执行状态变量(例如 @@error)、光标和临时表。
在申请更改当前数据库的请求中执行一个 USE 语句会导致所有后续请求都在新上下文中执行。类似地,更改批处理中 SET 选项的值则意味着所有后续执行都会在新设置值之下运行。
MARS 解除了一个会话下最多只能有一个待定请求的限制,在保留后向兼容性的同时,它为执行环境中的更改定义了更为精细的语义。
此时您对 MARS 是什么可能已经有了一个模糊的概念。简单说来,它就是能够让多个待定请求处于一个 SQL Server 连接之下的能力。多数情况下,这可以直接理解为在相同会话内部执行其他操作的同时存在多个默认结果集(流水游标)的能力。
分清 MARS 不是什么也很重要:
• |
平行执行:尽管 MARS 可以在一个连接之下提交多个请求,但这并不意味着这些请求会在服务器内平行执行。MARS 会在连接中的多个存在的请求之间复合执行线程,在预先定义好的点上交叉执行。 |
• |
光标替换:前面说过,在一些情况下光标可以作为缺少 MARS 的合适解决办法;它对于迁移这些情境使用 MARS 可能比较有效。但是,这并不意味着当前使用的所有光标都应当转为 MARS。 |
默认情况下,在对 SQL Server 2005 使用支持 MARS 的客户端驱动程序时,前面一节中包含的所有代码片断都“可以工作”。在支持 MARS 的连接下,上面所说的死锁情境(应用程序被挂起)现在也可以成功运行了。
支持 MARS 的客户端驱动程序如下:
• |
SQL Native Client 中包含的 SQLODBC 驱动程序 |
• |
SQL Native Client 中包含的 SQLOLEDB 驱动程序 |
• |
.NET Framework 版本 2.0 中包含的 SqlClient .Net Data Provider |
默认情况下,这些驱动程序将会建立支持 MARS 的连接。假如出于某种原因,您想要建立保留下级驱动程序行为的连接,每个 API 都提供了一个请求非 MARS 连接的选项。
SqlClient 提供了 MultipleActiveResultSets 连接字符串选项。假如此项设置为 false,则不允许对会话使用 MARS。假如此项设置为 true 或忽略此项,则可以使用 MARS。
同样地,ODBC 提供了一个 SQL_COPT_SS_MARS_ENABLED 连接选项,而 OLE-DB 提供了一个 SSPROP_INIT_MARSCONNECTION 选项。再强调一次,仅当需要禁用 MARS 时(因为默认是启用 MARS 的)才需要使用这些选项。
注意只有 SQL Native Client 版的 ODBC 和 OLEDB 提供程序才提供 MARS。旧版的提供程序还没有增强到支持 MARS。不用说,新版驱动程序在连接到 SQL Server 2000 或 更早版本的服务器时也不支持 MARS。
在更深层含义上,MARS 是关于在一个连接内启用多个请求的交叉执行。也就是说,允许成批执行,并且在执行中还允许执行其他请求。请注意,MARS 是在交叉执行而不是平行执行的意义上定义的。
MARS 结构允许多个批处理以交叉方式执行,尽管执行只能在定义良好的点切换。事实上,大多数语句都必须运行在一个批处理中。只有以下语句可以在完成之前交叉执行:
• |
SELECT |
• |
FETCH |
• |
READTEXT |
• |
RECEIVE |
• |
BULK INSERT(或 bcp 接口) |
• |
异步光标填充 |
它的确切含义是什么?它的含义是必须等到作为存储过程或批处理的一部分执行的不在此列表中的任何其他语句运行完毕,执行才可以被切换到其他 MARS。
作为示例,可以设想有一个提交了较长的运行中 DML 语句的批处理;假定就是一个 UPDATE 语句(它会影响几十万个记录)。假如正在执行这个语句时,提交了第二个批处理,那么它的执行就要等到 UPDATE 语句完成时才会开始。
另一方面,假如 SELECT 语句是第一个提交的语句,那么 UPDATE 语句就可以在 SELECT 语句中运行。但是,在 DML 操作完成之前,不会为 SELECT 语句生成任何新行。
这再一次说明了 MARS 交叉处理而不是平行处理请求。交叉处理不会受到请求语句位置的影响,无论它是位于批处理中、位于 EXEC 语句中还是位于存储过程中。
注意只要开始生成行,RECEIVE 语句就可以交叉处理。对于在 WAITFOR 子句中执行的 RECEIVE 语句,当该语句处于等待状态时,它是不可交叉处理的。
注意只有在 SET XACT_ABORT ON 之下执行,并且插入操作的表目标中没有定义任何触发器,或者指定了不引发触发器的选项,Bulk 操作才可以交叉执行。RECEIVE 只有在 XACT_ABORT 设置为 ON 是才可以交叉执行。
注意执行托管代码时,无法交叉执行用任何 .Net 语言编写的存储过程。假如使用了 inproc 数据提供程序,那么已经执行的批处理就要受到语句的交叉执行和原子执行的普通规则约束。
上面说过,MARS 是 SQL Server 数据访问 API 的默认处理模式。它的执行模式 – 不像服务器端光标 – 支持大批量语句,并且还可以调用存储过程和动态 sql 操作。对于在其中生成结果的“流水游标”模式,默认结果集 (MARS) 的性能会优于服务器端光标。
尽管其中还有某种很好的打印。默认结果集会“尽可能快地”生成结果。只要客户端驱动程序或应用程序正在消耗所生成的结果,就会产生这种情况。假如应用程序没有消耗结果,服务器端缓冲区会被填满,处理就会被挂起,直到结果被消耗。当执行被挂起时,很多资源会被占用:数据和架构被锁定,服务器工作线程被占用(包括堆栈和其他相关内存)。请注意,这种情况不仅限于 MARS;当一个请求生成了没有被快速消耗的默认结果集时,它代表 SQL Server 2000 和更早版本所导致的开销是相同的。MARS 并不意味着在流水游标开销方面的改进。
对于服务器端光标,并不存在这种资源占用的情况。在某种程度上有些关系,根据请求的光标类型,可能可以使用默认结果集无法使用的其他语义,即结果的可滚动性和可更新性。
由于上面已经介绍了请求的处理过程,现在我们直接推断从 SQL Server(假定不要求可滚动性和可更新性)检索结果的用法指南:假如应用程序能够快速地消耗结果,那么 MARS 下的默认结果集就可以提供最好的性能和开销特点。假如应用程序消耗结果的速度比较缓慢,则建议使用服务器端光标,尤其是 FAST_FORWARD 光标。
在大多数情况下,适合使用 MARS 默认结果集。那么有什么缓慢消耗结果的示例呢?想像一下执行返回结果的批处理或存储过程的应用程序,行的消耗取决于数据库外部操作(例如用户输入、UI 操作、与其他任务同步,等等)的完成。一般说来,请求长时间处于待定状态会影响应用程序和 SQL Server 的可伸缩性。
MARS 的引入改变了数据库引擎内部很多现有的假设,包括事务语义和一个事务内的并发操作。
尽管 OLE-DB 在会话中有一个事务处于活动状态时会禁止隐式产生连接,并且 ODBC 也经常使用“连接繁忙”来拒绝其他请求,但在支持 MARS 的世界里,这些事情都可以成功做到。假如会话中有一个活动事务,所有新的请求都会运行在指定事务之下;假如会话中没有活动事务,那么每个批处理都会运行在 auto commit 模式下,这意味着每个执行的语句都运行在它自己的事务之下。
SqlClient 托管提供程序的模型更为明显。Specific SqlCommands 需要关联给定的 SqlTransaction 对象,以便指定特定请求运行在哪个事务之下。
一般说来,事务确定了多个用户之间的隔离。但是在 MARS 下,多个请求可以运行在相同的事务之下,这使得请求之间彼此兼容,并避免产生“重新捕获”一节中所述的死锁。但是,假如在相同事务下有两个请求之间存在冲突操作,又会怎么样呢?
有几种可能的情况,下面分别说明:
• |
一个请求正在读取某些结果(比如说 SELECT、FETCH 或 READTEXT)。另外一个请求修改了正在读取的数据(比如说 DML 操作)。在这种情况下,尽管更改操作成功,但读取操作独立于更改,因此所有读取的数据看来就是读取操作开始时的状态。请注意,只有读取操作先于修改操作开始,才有可能产生这种情况。假如 DML 语句首先开始执行,那么读取操作就要等到后面才执行,这样就可以看到所有更改了。 |
• |
两个请求尝试修改相同数据。对于语句的原子数规则,必须在 DML 语句运行完毕后才能运行其他语句。因此,尝试修改数据的两个批处理永远不会交叉执行。请求会按照顺序执行,而结果会反应执行的顺序。请注意,假如客户端应用程序是多线程的,这可能会产生非确定性行为。 |
• |
一个请求正在读取数据(比如说 SELECT、FETCH 或 READTEXT),但任一基本对象的架构被修改了(比如说 DML 操作)。在这种情况下,DDL 操作会失败,因为在相同的事务下存在冲突的待定请求。请注意,这种行为也适用于在 RECEIVE 语句产生结果时尝试更改服务代理队列架构的情况。 |
• |
重叠操作发生在被批量插入的表上。BULK INSERT(或 bcp、IRowsetFastLoad)可以非原子运行,即可以与其他语句交叉执行。但是,在 BULK INSERT 的对象目标上不能并发执行任何 DDL、DML 或读取操作。这种情况下会产生一个错误,因为在相同的事务下存在冲突的待定请求。 |
请记住,上述情况仅适用于请求运行在相同事务下。对于运行在不同事务下的请求,则适用常规的锁定、阻止或隔离语义。
顺便说一下,事务框架实现的 MARS 下的事务语义现在也用于绑定会话和 DTC。这意味着尽管它以前只能在没有请求处于待定状态时更改事务上下文,现在则能够在支持非原子运行的一组相同语句期间切换上下文。同样地,在 DML、DDL 和其他必须原子运行的语句执行时,不能切换事务上下文。
注意假如在给定事务下存在待定的请求,则尝试提交事务将会失败。
事务保存点通常用于允许在一个事务内部部分回滚。通常,应用程序开始一个事务,设置保存点,进行某些工作,然后假如工作成功则继续,不成功的话则回滚到保存点。以下示例显式了保存点运行在相同事务下两个事务的交叉:
时间 | 批处理 1 | 批处理 2 |
T1 |
开始事务; |
|
T2 |
删除 dbo.operations,其中 operation_id=5; |
|
T3 |
保存事务 sp1; | |
T4 |
插入 dbo.operations 默认值; | |
T5 |
删除 dbo.operations,其中 operation_id=10; |
|
T6 |
插入 dbo.operations 默认值; | |
T7 |
假如 @@error>0 回滚事务 sp1; | |
... |
||
Tn |
提交事务; |
在上述示例中,第一个请求开始一个事务,并执行某项工作(删除一行)。然后批处理 2 开始运行(在相同事务下),并尝试设置保存点,以确保事务内的一组给定的语句要么成功执行,要么原子失败。
但是,在批处理 2 执行这两个语句的时候,一个来自批处理 1 的删除操作被交叉执行。假定批处理 2 中出现了一个错误,请求将会尝试回滚到保存点 sp1。但是,这也会“静态”回滚批处理 1 在 T5 时执行的删除操作。
为了避免这种不可预测和难以调试的情况发生,当一个事务中有多个活动请求正在运行时,MARS 不允许设置保存点、回滚到保存点和提交事务。假如上述这两个请求按照顺序执行,操作会成功,但在指定的并发请求如上述方式交叉执行的情况下,在批处理 2 中设置保存点的尝试将会失败,并产生一个错误。
前面说过,看起来执行环境好像是一个遍布整个会话的全局环境。在 MARS 下,假如多个请求同时更改环境会发生什么?请看如下示例:
时间 | 批处理 1 | 批处理 2 | 批处理 3 |
T1 |
使用 operations; |
||
T2 |
使用 msdb; |
||
T3 |
从 dbo.operations 中选择 operation_id; |
从 sys.objects 中选择名称; |
|
... |
|||
Tn |
从 sys.objects 中选择名称; |
上面的示例显式了三个批处理运行在相同的连接下。批处理 1 和批处理 2 更改数据库上下文,然后运行 SELECT 语句。批处理 3 则在稍后的时间运行 SELECT 语句,但没有指定数据库上下文。假如执行环境是连接的真正全局状态,那么上述组合的结果对于应用程序开发来说将是令人困惑和不可预测的。
MARS 具有请求级别执行环境和会话级别默认执行环境。当请求开始执行时,会话级别环境就会被克隆成请求级别环境。在整个批处理完成之后,所产生的环境会被复制回会话级别默认执行环境。
在这种语义下,请求的执行顺序(SQL Server 2000 中唯一允许的行为)提供了一个单一会话全局执行环境的假象。但是,在并发 MARS 请求下,一个请求所做的更改并不会影响其他并发执行的请求。
在上述示例中,会话环境被复制到批处理 1 和批处理 2 上,然后这两个批处理的 SELECT 语句就会运行在所期望的数据库上下文中。完成之后,这两个批处理就会复制(和覆盖)会话环境。请注意,在在这种情况下,所产生的会话数据库将取决于批处理 1 和 2 的完成时间。假如批处理 3 在批处理 1 和批处理 2 完成之后开始执行,那么根据前面两个请求的时间,返回的结果将对应于“operations”或“msdb”数据库。
请注意在 MARS 下编写多个批处理程序的上下文复制语义。上下文的复制包括 SET 选项和其余的执行环境。
注意假如某个批处理被取消,执行环境默认会被复制回取消请求被确认时的会话环境。
MARS 支持几种新的情境,但由于这通常是一些具有强大功能的情况,因此它也增加了您射死自己脚步的机会。请看以下示例。
传统上,允许对 DML 语句使用触发器。SQL Server 2005 扩展了这种模型,它允许在 DDL 语句上定义触发器。现在,我们来看看应用程序决定向调用方放回来自触发器内部的结果(当然不是您将要按照这个可怕的练习操作的)。触发器主体定义中包含一个 SELECT 语句。从应用程序角度看来,伪代码应当类似如下所示:
Request 1: update table operations; // this will return results from the trigger. For each row returned from the trigger { Request 2: Read from some other table based on current row; }
利用上述示例,我们创建了一种新型的应用程序死锁。从 request 1 每返回一行,Request 2 都会尝试执行。但是,由于 request 1 是一个 DML 语句,因此必须等它运行完毕,任何其他语句才可以执行。相同情况也适用于 DDL 语句。在这种情况下,要等到 request 1 完成后 request 2 才会运行。但是,request 1 的完成又取决于每个 request 2 的执行。
MARS 通过在死锁检测链中添加阻止网络操作,从而解决了这个问题。对于上述情境,SQL Server 的死锁监视器将会检测环境,然后终止 Request 2 并显示一条错误信息,指出会话正忙于响应其他请求。
作为一般性规则,请记住哪些语句必须原子运行,并确保没有任何操作会阻止它们前进。更重要的是,确保它们不会被依赖于前面语句完成的操作所阻止。
从触发器返回结果是在这种环境中运行的最简单的方式。由于这个原因和其他一些原因,我们强烈建议不要从触发器返回结果。这包括没有分配子句的 SELECT 语句、没有分配子句的 FETCH 语句、PRINT 语句以及其他在 NOCOUNT 设置为 OFF 时运行的语句。
恰如我们看到的,MARS 已经改变了 SQL Server 引擎内的一些核心假设。很重要的一点是,在监视和诊断 SQL Server 实例时请记住一些新假设。
在 SQL Server 中,一个 spid 代表一个会话。由于以前版本中没有 MARS,因此很普遍地就将 spid 与请求关联在一起。普遍想到要检索某个给定 spid 的 SQL 文本。普遍在多个系统过程中查找 spid 的执行统计。所有这些对于支持 MARS 的情境来说可能不再足够。
尽管系统进程继续显示某个会话的信息,我们还是实现了一些改进以帮助监视 MARS。
新的“动态管理视图”(DMV) sys.dm_exec_sessions 代表了会话信息的新视图,包括会话默认执行环境。在这种视图下,传统上的 spid 反映在 session_id 列下。
同时,sys.dm_exec_connections 也可用于显示所有已经建立的指向服务器的物理和逻辑连接。逻辑连接是为运行在 MARS 下的每个请求建立的会话中的虚拟管道。对于逻辑连接来说,parent_connection_id 列是被填充的。普通 session_id 列也显示了一个会话中多个逻辑连接的关系。
一个新的 DMV(sys.dm_exec_requests)表示每个会话下可用请求的详细列表。
还引入了一个新的内部函数 current_request_id(),以便可以通过编程方式找到当前执行请求的 id。.这类似于现有的 @@spid 函数。
SQL Server 2005 中的 Multiple Active Result Sets (MARS) 支持增加了开发 SQL Server 应用程序的选项。它带来了光标编程模型与相关引擎的默认处理模型的性能和功能。
MARS 为使用多个连接来克服缺少 MARS 问题的一些应用程序提供了很好的替代办法。但是,有时也不一定使用 MARS,因为多个连接确实在服务器中提供了平行执行的能力(假如它们没有在相同事务中登记的话)。
尽管在很多情况下,MARS 可以作为服务器端光标的替代办法,并可以提供一些性能改进,但它并不能替代光标。恰如本白皮书中介绍的,在很多情况下 MARS 是很好的替代办法,但也有很多情况使用光标会更为合适。
简单说来,MARS 是一个编程模型增强,它允许多个请求可以在服务器中交叉执行。尽管它不代表可以在服务器中平行执行,但假如正确使用的话它确实可以产生一些性能优点。