使用内存优化表的事务

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

本文介绍特定于内存优化表和本机编译存储过程的事务的所有方面。

SQL Server 中的事务隔离级别以不同的方式应用到内存优化表与基于磁盘的表,并且基础机制不同。 了解差异可帮助程序员设计高吞吐量系统。 在所有情况下,事务完整性的目标是相同的。

有关特定于内存优化表事务的错误条件,请参阅 冲突检测和重试逻辑 部分。

有关常规信息,请参阅 SET TRANSACTION ISOLATION LEVEL

悲观与乐观

内存优化表与基于磁盘的表之间的功能差异来自对事务完整性的悲观与乐观方法。 内存优化表使用乐观方法:

  • 悲观方法使用锁来阻止发生潜在冲突。 执行语句时使用锁,提交事务时释放锁。

  • 乐观方法在冲突发生时检测到这些冲突,并在提交时间执行验证检查。

    • 内存优化表不会发生错误 1205(死锁)。

乐观方法的开销较小,通常效率更高,部分原因是大多数应用程序中事务冲突并不常见。 悲观方法和乐观方法之间的主要功能差异在于,如果发生冲突,使用悲观方法时,你需要等待,而使用乐观方法时,其中一个事务将失败并需要客户端重试。 当隔离级别REPEATABLE READ生效时,功能差异会更大,而在SERIALIZABLE级别时差异最大。

事务启动模式

SQL Server 使用以下模式进行事务启动:

  • 自动提交。 简单的查询或 DML 语句在开始时隐式打开事务,语句的末尾隐式提交事务。 “自动提交”为默认设置

    在自动提交模式下,通常不需要在FROM 子句中的内存优化表上指定关于事务隔离级别的表提示。

  • 显式。 Transact-SQL 包含代码 BEGIN TRANSACTION,以及最终的代码 COMMIT TRANSACTION。 可以将两个或多个语句关联到同一事务中。

    在显式模式下,必须使用数据库选项 MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT 或对子句中 FROM 内存优化表上的事务隔离级别进行表提示编码。

  • 隐式。 当SET IMPLICIT_TRANSACTION ON生效时启动。 此选项隐式执行 0,相当于在每个UPDATE语句之前显式地执行BEGIN TRANSACTION,如果@@TRANCOUNT0的话。 因此,最终需要由您的 T-SQL 代码明确调用 COMMIT TRANSACTION

  • ATOMIC 块ATOMIC 块中的所有语句始终在单个事务中运行。 成功时 ATOMIC 块的操作将作为一个整体提交,或者失败时这些操作将全部回滚。 每个本机编译的存储过程都需要一个 ATOMIC 块。

具有显式模式的代码示例

以下解释的 Transact-SQL 脚本使用:

  • 一个显式事务。
  • 内存优化表,名为 dbo.Order_mo.
  • READ COMMITTED事务隔离级别上下文。

因此,您需要在内存优化表上添加表提示符。 提示必须是 SNAPSHOT 或更高程度隔离的级别。 对于代码示例,提示为 WITH (SNAPSHOT). 如果删除此提示,脚本遇到错误 41368,因此自动重试不适用:

错误 41368

仅支持在自动提交事务中使用 READ COMMITTED 隔离级别访问内存优化表。 不支持显式或隐式事务。 使用表提示,例如 WITH (SNAPSHOT),为内存优化表提供支持的隔离级别。

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN TRANSACTION; -- Explicit transaction.

-- Order_mo is a memory-optimized table.
SELECT *
FROM dbo.Order_mo AS o WITH (SNAPSHOT) -- Table hint.
     INNER JOIN dbo.Customer AS c
         ON c.CustomerId = o.CustomerId;

COMMIT TRANSACTION;

可以使用MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT数据库选项来避免需要WITH (SNAPSHOT)提示。 将此选项设置为 ON时,在较低隔离级别下访问内存优化表时会自动提升为 SNAPSHOT 隔离。

ALTER DATABASE CURRENT
    SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT = ON;

行版本控制

内存优化表使用高度复杂的行版本化系统,使乐观方法在最严格的隔离级别SERIALIZABLE下也能高效运行。 有关详细信息,请参阅内存优化表简介

READ_COMMITTED_SNAPSHOTSNAPSHOT 隔离级别生效时,基于磁盘的表间接地具有行版本控制系统。 此系统基于 tempdb,而内存优化的数据结构带有内置的行版本控制,以实现最大效率。

隔离级别

下表按照从低到高的顺序列出了可能的事务隔离级别。 有关可能发生的冲突以及用于处理这些冲突的重试逻辑的详细信息,请参阅 冲突检测和重试逻辑

隔离级别 说明
READ UNCOMMITTED 不可用:无法在“读取未提交的隔离”下访问内存优化表。 如果将会话级别 TRANSACTION ISOLATION LEVEL 设置为 READ UNCOMMITTED,并使用表提示 WITH (SNAPSHOT) 或将数据库设置 MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOTON,则仍然可以在 SNAPSHOT 隔离级别下访问内存优化表。
READ COMMITTED 仅当自动提交模式生效时,内存优化表才支持此隔离级别。 如果在SNAPSHOT隔离下,仍然可以通过将会话级别的TRANSACTION ISOLATION LEVEL设置为READ COMMITTED,并使用WITH (SNAPSHOT)表提示或将数据库设置MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT设置为ON来访问内存优化表。

如果将数据库选项 READ_COMMITTED_SNAPSHOT 设置为 ON,则在 READ COMMITTED 隔离级别下,不能在同一语句中同时访问内存优化表和基于磁盘的表。
SNAPSHOT 受内存优化表的支持。

在内部,SNAPSHOT 是内存优化表中要求最低的事务隔离级别。

SNAPSHOT 使用的系统资源比 REPEATABLE READSERIALIZABLE 少。
REPEATABLE READ 受内存优化表的支持。 隔离 REPEATABLE READ 提供的保证是,在提交时,任何并发事务都没有更新此事务读取的任何行。

由于乐观模型,并发事务不会被阻止更新本事务读取的行。 相反,在提交时,此事务会验证REPEATABLE READ,确保隔离尚未侵犯。 如果已违反,此事务将回滚,并且必须重试。
SERIALIZABLE 受内存优化表的支持。

由于隔离非常严格,因而被命名为Serializable,因为它几乎类似于事务是串行运行的,而不是并发运行的。

事务阶段和生命周期

涉及内存优化表时,事务的生存期将经历下图所示的阶段:

显示内存中事务生存期的关系图。

下面是阶段的描述。

第 1 阶段(共 3 阶段:常规处理)

  • 此阶段执行查询中的所有查询和 DML 语句。

  • 在此阶段,语句从事务的逻辑开始时间起将看到内存优化表的版本。

第 2 阶段(共 3 阶段:验证)

  • 验证阶段开始时分配结束时间,该时间将事务标记为逻辑上完成。 此完成使事务的所有更改对依赖于此事务的其他事务可见。 在成功提交此事务之前,依赖事务无法提交。 此外,保存此类依赖项的事务无法将结果集返回给客户端,以确保客户端只看到已成功提交到数据库的数据。

  • 此阶段包括可重复读验证和可序列化验证。 对于可重复读取校验,它会检查事务是否更新了读取的任何行。 为了进行可序列化的验证,它会检查事务是否将行插入到扫描的任何数据范围内。 按隔离级别和冲突中的表所述,使用快照隔离时,可能会同时发生可重复读验证和可序列化验证,以验证唯一约束和外键约束的一致性。

第 3 阶段/共 3 阶段:提交处理

  • 在提交阶段,进程将更改写入持久表的日志,并将日志写入磁盘。 然后,进程将控制权返回到客户端。

  • 提交处理完成后,系统会通知所有依赖的事务,它们可以提交。

与往常一样,尽可能简化和缩短事务工作单位,以满足数据需求的有效性。

冲突检测和重试逻辑

两种类型的与事务相关的错误条件可能会导致事务失败并回滚。 在大多数情况下,在出现此类故障后,你需要重试事务,类似于发生死锁时。

  • 并发事务之间的冲突。 这些冲突(包括更新冲突和验证失败)可能会因为事务隔离级别冲突或约束冲突而发生。

  • 依赖项失败。 这些失败是由于你依赖的事务无法提交,或者依赖项数量增长过大。

以下错误条件可能会导致事务在访问内存优化表时失败。

错误代码 说明 原因
41302 尝试更新自从启动现有事务以来,已在其他事务中更新的行。 如果两个并发事务试图同时更新或删除同一行,则会发生此错误条件。 这两个事务之一收到此错误消息,需要重试。
41305 可重复读验证失败。 从内存优化表读取此事务的行已由在提交此事务之前提交的另一个事务更新。 使用 REPEATABLE READSERIALIZABLE 隔离时也可能发生此错误,如果并发事务的操作会导致违反 FOREIGN KEY 约束。

这种并发的外键约束冲突很少见,一般表示应用程序逻辑或数据输入出现了问题。 但是,如果约束涉及 FOREIGN KEY 的列没有索引,则也会发生此错误。 因此,始终在内存优化表中的外键列上创建索引。

有关外键冲突导致的验证失败的更详细注意事项,请参阅 SQL Server 客户顾问团队 针对内存优化表上的验证错误 41305 和 41325 的注意事项
41325 可序列化验证失败。 将新行插入到了现有事务之前已扫描的区域。 我们将此称为虚拟行。 使用SERIALIZABLE隔离时可能会发生此错误;如果并发事务的操作导致违反PRIMARY KEYUNIQUEFOREIGN KEY约束,也会发生此错误。

这种并发的约束冲突很少见,一般表示应用程序逻辑或数据输入出现了问题。 但是,与可重复读取验证失败类似,如果 FOREIGN KEY 存在约束且涉及的列没有索引,则也会发生此错误。
41301 依赖项失败:依赖另一个事务,但该事务随后无法提交。 此事务(Tx1)通过读取由Tx2写入的数据,在另一个事务(Tx2)的验证或提交处理阶段,对该事务(Tx2)产生了依赖。 Tx2 然后无法提交。 提交失败的最常见原因是 Tx2 可重复读取(41305)和可序列化的(41325)验证失败。 不太常见的原因是日志 IO 失败。
4182341840 已达到内存优化表和表变量中的用户数据配额。 错误 41823 适用于 Azure SQL 数据库中的 SQL Server Express、Web 和标准版本和单一数据库。 错误 41840 适用于 Azure SQL 数据库中的弹性池。

在大多数情况下,这些错误表明达到了用户数据的最大尺寸。 若要解决此错误,请从内存优化表中删除数据。 但是,在极少数情况下,此错误是暂时性的。 首次遇到这些错误时重试。

同此列表中的其他错误一样,错误 41823 和 41840 会导致活动事务中止。
41839 事务超出了最大提交依赖项数目。 给定事务(Tx1)可以依赖的事务数有限制。 这些事务是传出依赖项。 此外,可以依赖于给定事务的事务数限制(Tx1)。 这些事务是传入依赖项。 两个限制均为 8。

此类失败最常见的情况是,大量读取事务访问由单个写入事务写入的数据。 如果读取事务都对相同数据执行大型扫描,并且写入事务的验证或提交处理耗时较长,则达到此条件的可能性会增加。 例如,写入事务在可序列化隔离下执行大型扫描(这会增加验证阶段的长度),或者事务日志放置在慢速日志 IO 设备上(这会增加提交处理的长度)。 如果读取事务执行大型扫描,但预期只访问几行,则可能缺少索引。 同样,如果写入事务使用可序列化隔离并执行大型扫描,但预期只访问几行,则此条件也表示缺少索引。

可以使用跟踪标志 9926 来取消对提交依赖项数量的限制。 仅在确认没有缺失索引后仍遇到此错误条件时使用此跟踪标志,因为它可能会在上述情况下屏蔽这些问题。 另一个警告是,复杂的依赖项关系图,其中每个事务具有大量传入和传出依赖项,而单个事务具有许多依赖项层,可能会导致系统中效率低下。

适用于: SQL Server 2016 (13.x)。 更高版本的 SQL Server 和 Azure SQL 数据库对提交依赖项的数量没有限制。

重试逻辑

如果某个事务因上述任何条件而失败,请重试该事务。

可以在客户端或服务器端实现重试逻辑。 在客户端上实现重试逻辑以提高效率。 此方法还有助于在发生故障之前处理事务返回的结果集。

重试 T-SQL 代码示例

对于不向客户端返回结果集的事务,仅对 T-SQL 使用服务器端重试逻辑。 否则,重试可能会将额外的结果集返回给客户端。

以下的 T-SQL 脚本示例展示了如何编写用于解决与内存优化表相关事务冲突错误的重试逻辑。

-- Retry logic, in Transact-SQL.
DROP PROCEDURE If Exists usp_update_salesorder_dates;
GO

CREATE PROCEDURE usp_update_salesorder_dates
AS
BEGIN
    DECLARE @retry AS INT = 10;

    WHILE (@retry > 0)
    BEGIN

        BEGIN TRY
            BEGIN TRANSACTION;

            UPDATE dbo.SalesOrder_mo WITH (SNAPSHOT)
            SET OrderDate = GETUTCDATE()
            WHERE CustomerId = 42;

            UPDATE dbo.SalesOrder_mo WITH (SNAPSHOT)
            SET OrderDate = GETUTCDATE()
            WHERE CustomerId = 43;

            COMMIT TRANSACTION;

            SET @retry = 0; -- Stops the loop.
        END TRY

        BEGIN CATCH
            SET @retry - = 1;

            IF (@retry > 0
                AND ERROR_NUMBER() IN (41302, 41305, 41325, 41301, 41823, 41840, 41839, 1205))
                BEGIN
                    IF XACT_STATE() = -1
                        ROLLBACK TRANSACTION;

                    WAITFOR DELAY '00:00:00.001';
                END
            ELSE
                BEGIN
                    PRINT 'Suffered an error for which Retry is inappropriate.';
                    THROW;
                END
        END CATCH

    END -- While loop
END
GO

-- EXECUTE usp_update_salesorder_dates;

跨容器事务

如果一个事务是跨容器事务,则满足以下条件:

  • 从解释的 Transact-SQL 访问内存优化表。
  • 当事务已打开时执行本机过程(XACT_STATE() = 1)。

术语“跨容器”来自事务跨两个事务管理容器运行的事实。 一个容器管理基于磁盘的表,另一个容器管理内存优化表。

在单个跨容器事务中,可以使用不同的隔离级别来访问基于磁盘的表和内存优化表。 通过显式表提示(如 WITH (SERIALIZABLE) 或数据库选项 MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT)来表达这种差异。 此选项将内存优化表的隔离级别隐式提升为快照(如果 TRANSACTION ISOLATION LEVEL 配置为 READ COMMITTEDREAD UNCOMMITTED)。

在以下 Transact-SQL 代码示例中:

  • 基于磁盘的表 Table_D1 通过 READ COMMITTED 隔离级别进行访问。
  • 使用Table_MO7隔离级别访问内存优化表SERIALIZABLETable_MO6 没有具体的隔离级别,因为插入永远保持一致,实际上是在可序列化隔离级别下执行的。
-- Different isolation levels for
-- disk-based tables versus memory-optimized tables,
-- within one explicit transaction.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO

BEGIN TRANSACTION;

-- Table_D1 is a traditional disk-based table, accessed using READ COMMITTED isolation.
SELECT *
FROM Table_D1;

-- Table_MO6 and Table_MO7 are memory-optimized tables.
-- Table_MO7 is accessed using SERIALIZABLE isolation,
-- while Table_MO6 does not have a specific isolation level.

INSERT INTO Table_MO6
SELECT *
FROM Table_MO7 WITH (SERIALIZABLE);

COMMIT TRANSACTION;

限制

  • 内存优化表不支持跨数据库事务。 如果事务访问内存优化表,则事务无法访问任何其他数据库,但以下除外:

    • tempdb 数据库。
    • master 数据库进行只读访问。
  • 不支持分布式事务:使用 BEGIN DISTRIBUTED TRANSACTION时,事务无法访问内存优化表。

本机编译的存储过程

  • 在本地过程(ATOMIC)中,该块必须为整个代码块声明事务隔离级别,例如:

    ... BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, ...) ...

  • 不能在本机存储过程的正文中包含显式事务控制语句。 类似 BEGIN TRANSACTIONROLLBACK TRANSACTION 不允许的语句。

  • 有关使用 ATOMIC 块的事务控制的详细信息,请参阅 Native Procedures 中的原子块