跨云数据库的分布式事务

适用于: Azure SQL 数据库 Azure SQL 托管实例

本文介绍如何使用弹性数据库事务,这些事务使你能够运行 Azure SQL 数据库和 Azure SQL 托管实例的跨云数据库的分布式事务。 在本文中,“分布式事务”和“弹性数据库事务”这两个术语被视为同义词,可以互换使用。

概述

使用 Azure SQL 数据库和 Azure SQL 托管实例的弹性数据库事务,可以运行跨多个数据库的事务。 弹性数据库事务适用于使用 ADO.NET 的 .NET 应用程序,并且与你熟悉的使用 System.Transaction 类的编程体验相集成。 若要获取该库,请参阅 .NET Framework 4.6.1(Web 安装程序)。 此外,可在 Transact-SQL 中使用托管实例分布式事务。

应用程序可以连接到任何数据库来启动分布式事务,其中一个数据库或服务器会以透明方式协调分布式事务,如下图所示。

使用弹性数据库事务在 Azure SQL 数据库中执行分布式事务

常见方案

弹性数据库事务可让应用程序对多个不同数据库中存储的数据进行原子性更改。 SQL 数据库和 SQL 托管实例都支持 C# 和 .NET 中的客户端开发体验。 使用 Transact-SQL 的服务器端体验(以存储过程或服务器端脚本编写的代码)仅适用于 SQL 托管实例。

重要

不支持在 Azure SQL 数据库和 Azure SQL 托管实例之间运行弹性数据库事务。 弹性数据库事务只能跨 SQL 数据库中的一组数据库,或跨托管实例的一组数据库。

弹性数据库事务面向以下方案:

  • Azure 中的多数据库应用程序:在此方案中,数据垂直分区到 SQL 数据库或 SQL 托管实例中的多个数据库,使得不同种类的数据位于不同的数据库。 某些操作需要更改两个或更多数据库中保存的数据。 应用程序使用弹性数据库事务来协调数据库之间的更改并确保原子性。
  • Azure 中的分区数据库应用程序:在此方案中,数据层使用弹性数据库客户端库或自我分片,将数据水平分区到 SQL 数据库或 SQL 托管实例中的多个数据库。 常见的用例之一是在分片的多租户应用程序中,当更改涉及到多个租户时,需要执行原子更改。 例如,从一个租户转移到另一个租户,而两者位于不同的数据库。 第二种方案是以细致分片来适应大租户的容量需求,这又通常表示某些原子操作需要扩展到用于同一租户的多个数据库。 第三种方案是以原子更新来引用数据库之间复制的数据。 现在,可以跨多个数据库协调这几个方面的原子性事务操作。 弹性数据库事务使用两阶段提交,确保跨数据库的事务原子性。 如果单个事务一次涉及到的数据库少于 100 个,则适合采用此方案。 这些限制不是强制施加的,但是如果超出这些限制,弹性数据库事务的性能和成功率很有可能会下降。

安装和迁移

我们更新了 .NET 库 System.Data.dll 和 System.Transactions.dll,以提供弹性数据库事务功能。 DLL 确保必要时使用两阶段事务提交,以确保原子性。 若要使用弹性数据库事务来开始开发应用程序,请安装 .NET 4.6.1 或更高版本。 在旧版 .NET Framework 上运行时,事务无法升级为分布式事务,并会引发异常。

安装后,可以使用 System.Transactions 中的分布式事务 API 以及与 SQL 数据库和 SQL 托管实例的连接。 如果现有的 MSDTC 应用程序使用了这些 API,请在安装 4.6.1 Framework 之后,以 .NET 4.6 为目标重建现有的应用程序。 如果项目以 .NET 4.6 为目标,它们会自动使用新 Framework 版本中更新的 DLL,而结合 SQL 数据库或 SQL 托管实例连接的分布式事务 API 调用现在会成功。

请记住,弹性数据库事务不需要安装 MSDTC。 弹性数据库事务改为直接由该服务进行管理。 这可以大幅简化云方案,因为 MSDTC 的部署不需要使用分布式事务和 SQL 数据库或 SQL 托管实例。 第 4 部分更详细说明了如何将弹性数据库事务和所需的 .NET Framework 连同云应用程序一起部署到 Azure。

适用于 Azure 云服务的 .NET 安装

Azure 为托管 .NET 应用程序提供了多个产品。 不同产品的比较可见于 Azure 应用服务、云服务和虚拟机比较。 如果产品的来宾 OS 版本低于弹性事务所需的 .NET 4.6.1,需要将来宾 OS 升级到 4.6.1。

对于 Azure 应用服务,当前不支持升级到来宾 OS。 对于 Azure 虚拟机,只需要登录到 VM 并运行最新的 .NET framework 安装程序即可。 对于 Azure 云服务,需要将更高版本的 .NET 安装包括到部署的启动任务中。 在云服务角色上安装 .NET 中说明了概念和步骤。

请注意,与 .NET 4.6 的安装程序相比,.NET 4.6.1 的安装程序在 Azure 云服务上执行引导过程时,可能需要更多的临时存储空间。 为了确保安装成功,需要在 ServiceDefinition.csdef 文件中启动任务的 LocalResources 部分和环境设置中,增加 Azure 云服务的临时存储,如以下示例所示:

<LocalResources>
...
    <LocalStorage name="TEMP" sizeInMB="5000" cleanOnRoleRecycle="false" />
    <LocalStorage name="TMP" sizeInMB="5000" cleanOnRoleRecycle="false" />
</LocalResources>
<Startup>
    <Task commandLine="install.cmd" executionContext="elevated" taskType="simple">
        <Environment>
    ...
            <Variable name="TEMP">
                <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/LocalResources/LocalResource[@name='TEMP']/@path" />
            </Variable>
            <Variable name="TMP">
                <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/LocalResources/LocalResource[@name='TMP']/@path" />
            </Variable>
        </Environment>
    </Task>
</Startup>

.NET 开发体验

多数据库应用程序

以下示例代码使用熟悉的 .NET System.Transactions 编程体验。 TransactionScope 类在 .NET 中创建环境事务。 (“环境事务”是位于当前线程中的事务。)在 TransactionScope 内打开的所有连接都参与该事务。 如果有不同的数据库参与,事务自动提升为分布式事务。 通过设置完成范围来指示提交,即可控制事务的结果。

using (var scope = new TransactionScope())
{
    using (var conn1 = new SqlConnection(connStrDb1))
    {
        conn1.Open();
        SqlCommand cmd1 = conn1.CreateCommand();
        cmd1.CommandText = string.Format("insert into T1 values(1)");
        cmd1.ExecuteNonQuery();
    }
    using (var conn2 = new SqlConnection(connStrDb2))
    {
        conn2.Open();
        var cmd2 = conn2.CreateCommand();
        cmd2.CommandText = string.Format("insert into T2 values(2)");
        cmd2.ExecuteNonQuery();
    }
    scope.Complete();
}

分片数据库应用程序

SQL 数据库和 SQL 托管实例的弹性数据库事务还支持协调分布式事务,这需要使用弹性数据库客户端库的 OpenConnectionForKey 方法,打开扩大的数据层的连接。 假设需要保证事务一致性,使更改跨多个不同的分片键值。 与托管不同分片键值的分片的连接由 OpenConnectionForKey 来中转。 在一般情况下,可以连接到不同的分片,以确保事务保证需要分布式事务。 以下代码示例演示了此方法。 假设使用一个称为 shardmap 的变量代表来自弹性数据库客户端库的分片映射:

using (var scope = new TransactionScope())
{
    using (var conn1 = shardmap.OpenConnectionForKey(tenantId1, credentialsStr))
    {
        SqlCommand cmd1 = conn1.CreateCommand();
        cmd1.CommandText = string.Format("insert into T1 values(1)");
        cmd1.ExecuteNonQuery();
    }
    using (var conn2 = shardmap.OpenConnectionForKey(tenantId2, credentialsStr))
    {
        var cmd2 = conn2.CreateCommand();
        cmd2.CommandText = string.Format("insert into T1 values(2)");
        cmd2.ExecuteNonQuery();
    }
    scope.Complete();
}

Transact-SQL 开发体验

使用 Transact-SQL 的服务器端分布式事务仅适用于 Azure SQL 托管实例。 只能在属于同一服务器信任组的实例之间执行分布式事务。 在这种情况下,托管实例需要使用链接服务器来相互引用。

下面的示例 Transact-SQL 代码使用 BEGIN DISTRIBUTED TRANSACTION 来启动分布式事务。

    -- Configure the Linked Server
    -- Add one Azure SQL Managed Instance as Linked Server
    EXEC sp_addlinkedserver
        @server='RemoteServer', -- Linked server name
        @srvproduct='',
        @provider='MSOLEDBSQL', -- Microsoft OLE DB Driver for SQL Server
        @datasrc='managed-instance-server.46e7afd5bc81.database.chinacloudapi.cn' -- SQL Managed Instance endpoint

    -- Add credentials and options to this Linked Server
    EXEC sp_addlinkedsrvlogin
        @rmtsrvname = 'RemoteServer', -- Linked server name
        @useself = 'false',
        @rmtuser = '<login_name>',         -- login
        @rmtpassword = '<secure_password>' -- password

    USE AdventureWorks2022;
    GO
    SET XACT_ABORT ON;
    GO
    BEGIN DISTRIBUTED TRANSACTION;
    -- Delete candidate from local instance.
    DELETE AdventureWorks2022.HumanResources.JobCandidate
        WHERE JobCandidateID = 13;
    -- Delete candidate from remote instance.
    DELETE RemoteServer.AdventureWorks2022.HumanResources.JobCandidate
        WHERE JobCandidateID = 13;
    COMMIT TRANSACTION;
    GO

组合 .NET 和 Transact-SQL 开发体验

使用 System.Transaction 类的 .NET 应用程序可以将 TransactionScope 类与 Transact-SQL 语句 BEGIN DISTRIBUTED TRANSACTION 组合使用。 在 TransactionScope 内,执行 BEGIN DISTRIBUTED TRANSACTION 的内部事务将显式提升为分布式事务。 此外,在 TransactionScope 内打开第二个 SqlConnecton 时,它会被隐式提升为分布式事务。 在分布式事务启动之后,所有后续事务请求(无论是来自 .NET 还是来自 Transact-SQL)都将加入父分布式事务。 因此,由 BEGIN 语句启动的所有嵌套事务范围将在同一事务中结束,COMMIT/ROLLBACK 语句将对总体结果产生以下影响:

  • COMMIT 语句不会对由 BEGIN 语句启动的事务范围产生任何影响,也就是说,在 TransactionScope 对象上调用 Complete() 方法之前,不会提交任何结果。 如果 TransactionScope 对象在完成前被销毁,则会回滚在该范围内执行的所有更改。
  • ROLLBACK 语句将导致回滚整个 TransactionScope。 之后,任何在 TransactionScope 中登记新事务的尝试都会失败,尝试在 TransactionScope 对象上调用 Complete() 也会失败。

下面是一个示例,其中使用了 Transact-SQL 将事务显式提升为分布式事务。

using (TransactionScope s = new TransactionScope())
{
    using (SqlConnection conn = new SqlConnection(DB0_ConnectionString)
    {
        conn.Open();
    
        // Transaction is here promoted to distributed by BEGIN statement
        //
        Helper.ExecuteNonQueryOnOpenConnection(conn, "BEGIN DISTRIBUTED TRAN");
        // ...
    }
 
    using (SqlConnection conn2 = new SqlConnection(DB1_ConnectionString)
    {
        conn2.Open();
        // ...
    }
    
    s.Complete();
}

下面的示例展示了在 TransactionScope 中启动第二个 SqlConnecton 后会被隐式提升为分布式事务的事务。

using (TransactionScope s = new TransactionScope())
{
    using (SqlConnection conn = new SqlConnection(DB0_ConnectionString)
    {
        conn.Open();
        // ...
    }
    
    using (SqlConnection conn = new SqlConnection(DB1_ConnectionString)
    {
        // Because this is second SqlConnection within TransactionScope transaction is here implicitly promoted distributed.
        //
        conn.Open(); 
        Helper.ExecuteNonQueryOnOpenConnection(conn, "BEGIN DISTRIBUTED TRAN");
        Helper.ExecuteNonQueryOnOpenConnection(conn, lsQuery);
        // ...
    }
    
    s.Complete();
}

SQL 数据库的事务

Azure SQL 数据库中支持跨不同服务器的弹性数据库事务。 当事务跨越服务器边界时,参与的服务器首先需要进入相互通信关系。 一旦建立了通信关系,任意两个服务器中的任何数据库都可以与另一服务器的数据库参与弹性事务。 当事务跨越两个以上的服务器时,任意服务器对之间的通信关系需要准备就绪。

使用以下 PowerShell cmdlet 来管理弹性数据库事务的跨服务器通信关系:

  • New-AzSqlServerCommunicationLink:使用此 cmdlet 在 Azure SQL 数据库中的两个服务器之间创建新的通信关系。 这种关系是对称的,这意味着一台服务器可使用另一台服务器启动事务。
  • Get-AzSqlServerCommunicationLink:使用此 cmdlet 来检索现有通信关系及其属性。
  • Remove-AzSqlServerCommunicationLink:使用此 cmdlet 来删除现有通信关系。

SQL 托管实例的事务

分布式事务支持跨多个实例中的数据库。 当事务跨越托管实例边界时,参与方实例之间需要相互存在安全性和通信关系。 这可以使用 Azure 门户、Azure PowerShell 或 Azure CLI 创建服务器信任组来实现。 如果实例不在同一个虚拟网络上,则必须配置虚拟网络对等互连,并且网络安全组入站和出站规则需要在所有参与方虚拟网络上允许端口 5024 和 11000-12000。

Azure 门户上的服务器信任组

下图显示了包含托管实例的服务器信任组,这些托管实例可以使用 .NET 或 Transact-SQL 执行分布式事务:

使用弹性事务在 Azure SQL 托管实例中执行分布式事务

监视事务状态

使用动态管理视图 (DMV) 监视正在进行的弹性数据库事务的状态和进度。 与事务相关的所有 DMV 都与 SQL 数据库和 SQL 托管实例中的分布式事务相关。 可以在此处找到相应的 DMV 列表:与事务相关的动态管理视图和函数 (Transact-SQL)

这些 DMV 特别有用:

  • sys.dm_tran_active_transactions:列出当前活动的事务及其状态。 UOW(工作单位)列可以标识属于同一分布式事务的不同子事务。 同一分布式事务中的所有事务具有相同的 UOW 值。 有关详细信息,请参阅 DMV 文档
  • sys.dm_tran_database_transactions:提供有关事务的其他信息,例如事务在日志中的位置。 有关详细信息,请参阅 DMV 文档
  • sys.dm_tran_locks:提供当前进行中事务所持有的锁的相关信息。 有关详细信息,请参阅 DMV 文档

限制

SQL 数据库中的弹性数据库事务当前存在以下限制:

  • 仅支持 SQL 数据库中跨数据库的事务。 其他 X/Open XA 资源提供程序和除 SQL 数据库以外的数据库无法参与弹性数据库事务。 这意味着,弹性数据库事务无法扩展到本地 SQL Server 和 Azure SQL 数据库。 对于本地的分布式事务,请继续使用 MSDTC。
  • 仅支持来自 .NET 应用程序的客户端协调事务。 目前已规划 T-SQL 的服务器端支持,例如 BEGIN DISTRIBUTED TRANSACTION,但尚未推出。
  • 不支持跨 WCF 服务的事务。 例如,有一个执行事务的 WCF 服务方法。 事务范围内的调用将失败,并显示异常 System.ServiceModel.ProtocolException

以下限制目前适用于 SQL 托管实例中的分布式事务(也称为弹性事务或本机支持的分布式事务):

  • 使用此技术时,仅支持托管实例中跨数据库的事务。
  • 不支持跨 WCF 服务的事务。 例如,有一个执行事务的 WCF 服务方法。 事务范围内的调用将失败,并显示异常 System.ServiceModel.ProtocolException
  • Azure SQL 托管实例必须是服务器信任组的一部分才能参与分布式事务。
  • 服务器信任组的限制影响分布式事务。
  • 参与分布式事务的托管实例需要具有通过专用终结点进行的连接(使用部署了专用终结点的虚拟网络中的专用 IP 地址),并且需要使用专用 FQDN 来相互引用。 客户端应用程序可以在专用终结点上使用分布式事务。 此外,当 Transact-SQL 利用引用了专用终结点的链接服务器时,客户端应用程序也可以在公共终结点上使用分布式事务。 下图对此限制进行了说明。

专用终结点连接限制