可序列化的隔离 - Amazon Redshift

可序列化的隔离

某些应用程序不仅需要并发查询和加载,还需要能同时对多个表或同一个表进行写入。在此环境中,并发 指的是重叠,而不是安排在完全相同的时间运行。如果两个事务中的第二个事务在第一个提交前开始,则将两个事务视为并发。并发操作可源自由同一用户或不同用户控制的不同会话。

注意

Amazon Redshift 支持默认的自动提交行为,其中,每个单独运行的 SQL 命令都将分别提交。如果您在某个事务数据块中包含一组命令(由 BEGINEND 语句定义),则该数据块将作为一个事务提交,以便您在必要时对其进行回滚。此行为的例外是 TRUNCATE 和 VACUUM 命令,这些命令可自动提交当前事务中所做的所有待定更改。

某些 SQL 客户端会自动发出 BEGIN 和 COMMIT 命令,因此客户端控制着是一组语句作为一个事务运行,还是每个单独的语句作为自己的事务运行。检查您正在使用的界面的文档。例如,使用 Amazon Redshift JDBC 驱动程序时,具有包含多个(分号分隔)SQL 命令的查询字符串的 JDBC PreparedStatement 将所有语句作为单个事务运行。相比之下,如果您使用 SQL Workbench/J 并设置 AUTO COMMIT ON,则如果您运行多个语句,每个语句都会作为自己的事务运行。

在 Amazon Redshift 中,以保护性方式支持并发写入操作,即对表使用写入锁定和可序列化的隔离原则。可序列化的隔离会保留一种错觉,即对某个表运行的事务是对该表运行的唯一事务。例如,两个同时运行的事务(T1 和 T2)必须生成与以下至少一个项相同的结果:

  • T1 和 T2 依次运行。

  • T2 和 T1 依次运行。

并发事务彼此不可见;它们不能相互检测对方的更改。每个并发事务将在事务开始时创建数据库快照。在大多数 SELECT 语句、DML 命令(例如 COPY、DELETE、INSERT、UPDATE 和 TRUNCATE)和以下 DDL 命令首次出现时,将在事务中创建数据库快照:

  • ALTER TABLE(添加或删除列)

  • CREATE TABLE

  • DROP TABLE

  • TRUNCATE TABLE

如果并发事务的任何序列执行产生与它们的并发执行相同的结果,则这些事务将被视为“可序列化的”且可安全运行。如果这些事务的任何序列执行均不产生相同结果,则执行会破坏可序列性的语句的事务将被中止并回滚。

系统目录表 (PG) 和其他 Amazon Redshift 系统表(STL 和 STV)在事务中未锁定。因此,DDL 和 TRUNCATE 操作引起的数据库对象更改在提交到任何并发事务时均可见。

例如,假设在两个并发事务(T1 和 T2)开始时,数据库中存在表 A。假设 T2 通过从 PG_TABLES 目录表中进行选择来返回表列表。然后 T1 删除表 A 并提交,T2 再次列出这些表。现在不再列出表 A。如果 T2 尝试查询已删除的表,则 Amazon Redshift 将返回“relation does not exist”错误。向 T2 返回表列表或检查表 A 是否存在的目录查询不受与针对用户表的操作相同的隔离规则的约束。

更新这些表的事务在读取已提交 隔离模式下运行。前缀为 PG 的目录表不支持快照隔离。

系统表和目录表的可序列化隔离

对于任何引用用户创建的表或 Amazon Redshift 系统表(STL 或 STV)的 SELECT 查询,还将在事务中创建数据库快照。不引用任何表的 SELECT 查询不会创建新的事务数据库快照。仅在系统目录表 (PG) 上操作的 INSERT、DELETE 和 UPDATE 语句也不会创建新的事务数据库快照。

如何修复可序列化的隔离错误

ERROR:1023 DETAIL:Redshift 中的表上的可序列化隔离冲突

当 Amazon Redshift 检测到可序列化的隔离错误时,您会看到错误消息,如下所示。

ERROR:1023 DETAIL: Serializable isolation violation on table in Redshift

要解决可序列化的隔离错误,您可以尝试以下方法:

  • 重试已取消的事务。

    Amazon Redshift 检测到并发工作负载不可序列化。它建议使应用程序逻辑中存在差异,而这些差异通常可以通过重试遇到错误的事务来解决。如果问题仍然存在,请尝试使用其他方法之一。

  • 将任何不必在同一个原子事务中的操作移到事务之外。

    当两个事务内的各个操作以可能影响另一个事务的结果的方式相互交叉引用时,此方法适用。例如,以下两个会话各自启动一个事务。

    Session1_Redshift=# begin;
    Session2_Redshift=# begin;

    每个事务中的 SELECT 语句的结果可能受另一个事务中的 INSERT 语句影响。换句话说,假设您以任何顺序连续运行以下语句。在每种情况下,如果不是同时运行事务,结果是 SELECT 语句之一另外返回一行。操作顺序运行时,没有哪种顺序可以产生与并发运行时相同的结果。因此,运行的最后一个操作会导致可序列化的隔离错误。

    Session1_Redshift=# select * from tab1; Session1_Redshift=# insert into tab2 values (1);
    Session2_Redshift=# insert into tab1 values (1); Session2_Redshift=# select * from tab2;

    在许多情况下,SELECT 语句的结果并不重要。换句话说,事务中操作的原子性并不重要。在这些情况下,将 SELECT 语句移到事务之外,如以下示例所示。

    Session1_Redshift=# begin; Session1_Redshift=# insert into tab1 values (1) Session1_Redshift=# end; Session1_Redshift=# select * from tab2;
    Session2_Redshift # select * from tab1; Session2_Redshift=# begin; Session2_Redshift=# insert into tab2 values (1) Session2_Redshift=# end;

    在这些示例中,事务中没有交叉引用。两个 INSERT 语句不会相互影响。在这些示例中,至少有一个顺序,其中事务可以顺序运行并产生与并发运行时相同的结果。这意味着事务是可序列化的。

  • 通过锁定每个会话中的所有表来强制序列化。

    LOCK 命令阻止可能导致可序列化隔离错误的操作。使用 LOCK 命令时,请确保执行以下操作:

    • 锁定受事务影响的所有表,包括受事务内部的只读 SELECT 语句影响的表。

    • 无论执行操作的顺序如何,都以相同的顺序锁定表。

    • 在执行任何操作之前,在事务开始时锁定所有表。

  • 对并发事务使用快照隔离

    将 ALTER DATABASE 命令与快照隔离功能结合使用。有关 ALTER DATABASE 的 SNAPSHOT 参数的更多信息,请参阅参数

ERROR:1018 DETAIL:关系不存在

当您在不同会话中运行 Amazon Redshift 并发操作时,您会看到错误消息,如下所示。

ERROR: 1018 DETAIL: Relation does not exist.

Amazon Redshift 中的事务遵循快照隔离。在事务开始后,Amazon Redshift 将拍摄数据库的快照。对于事务的整个生命周期,事务在快照中反映的数据库状态下运行。如果事务从快照中不存在的表读取,则会引发之前显示的 1018 错误消息。即使另一个并发事务在事务拍摄快照后创建表,该事务也无法从新创建的表中读取。

要解决此序列化隔离错误,您可以尝试将事务的起始位置移动到您所知道的该表存在的位置。

如果表是由另一个事务创建的,则此位置至少在该事务提交之后。此外,请确保没有提交可能已删除表的并发事务。

session1 = # BEGIN; session1 = # DROP TABLE A; session1 = # COMMIT;
session2 = # BEGIN;
session3 = # BEGIN; session3 = # CREATE TABLE A (id INT); session3 = # COMMIT;
session2 = # SELECT * FROM A;

作为 session2 的读取操作运行的最后一个操作会导致可序列化的隔离错误。当 session2 拍摄快照并且表已被提交的 session1 删除时,会发生此错误。换句话说,即使并发 session3 已创建表,session2 也不会看到该表,因为它不在快照中。

要解决此错误,您可以按以下方式对会话重新排序。

session1 = # BEGIN; session1 = # DROP TABLE A; session1 = # COMMIT;
session3 = # BEGIN; session3 = # CREATE TABLE A (id INT); session3 = # COMMIT;
session2 = # BEGIN; session2 = # SELECT * FROM A;

现在,当 session2 拍摄快照时,session3 已经提交,并且表位于数据库中。Session2 可以从表中读取,而不会出现任何错误。