【问题标题】:Oracle transaction isolationOracle 事务隔离
【发布时间】:2010-08-16 20:30:30
【问题描述】:

我有一个方法 SaveApp() 将停用现有记录并插入新记录。

void SaveApp(int appID)
{
   begin transaction;
   update;
   insert;
   commit transaction;
}

假设在数据库表 SalesApp 中,我有 2 条 appID 等于 123 的记录;

  1. 记录 1,appID 123,不活动
  2. 记录 2,appID 123,活动

如果我在两个线程中同时调用这个方法SaveApp(),第一个事务(我们称之为T1)将更新现有的两条记录,而第二个事务(我们称之为T2) 等待。

T1完成后,现在这张表里会有3条记录。然而,不知何故,T2 不知道新插入的记录,T2 中的更新查询只更新前两条记录,并插入第四条。

在这两个方法调用之后,在数据库中,我们现在将有 4 条记录,第 3 条和第 4 条都处于活动状态,这是错误的。

  1. 记录 1,appID 123,不活动
  2. 记录 2,appID 123,不活动
  3. 记录 3,appID 123,活动
  4. 记录 4,appID 123,活动

你知道有什么办法可以解决这个问题吗?我尝试过使用可序列化的隔离级别,但它不起作用。

谢谢!

【问题讨论】:

    标签: oracle transactions isolation-level


    【解决方案1】:

    您是否有另一个表,每个 AppId 包含一行,通过唯一或主键约束强制执行?如果是这样,请在父表上使用 select for update 来序列化每个 AppId 的访问。

    创建表:

    session_1> create table parent (AppId number primary key);
    
    Table created.
    
    session_1> create table child (AppId number not null references Parent(AppId)
      2      , status varchar2(1) not null check (status in ('A', 'I'))
      3      , InsertedAt date not null)
      4  /
    
    Table created.
    

    插入起始值:

    session_1> insert into Parent values (123);
    
    1 row created.
    
    session_1> insert into child values (123, 'I', sysdate);
    
    1 row created.
    
    session_1> insert into child values (123, 'A', sysdate);
    
    1 row created.
    
    session_1> commit;
    
    Commit complete.
    

    开始第一笔交易:

    session_1> select AppId from Parent where AppId = 123 for update;
    
         APPID
    ----------
           123
    
    session_1> update Child set Status = 'I' where AppId = 123 and Status = 'A';
    
    1 row updated.
    
    session_1> insert into child values (123, 'A', sysdate);
    
    1 row created.
    

    在提交之前,在第二个会话中,确保我们只看到第一行:

    session_2> select * from Child;
    
         APPID S INSERTEDAT
    ---------- - -------------------
           123 I 2010-08-16 18:07:17
           123 A 2010-08-16 18:07:23
    

    开始第二笔交易:

    session_2> select AppId from Parent where AppId = 123 for update;
    

    会话 2 现在被阻止,正在等待会话 1。并且不会继续。 提交会话 1 将取消阻止会话

    session_1> commit;
    
    Commit complete.
    

    我们现在看到的第 2 节:

         APPID
    ----------
           123
    

    完成第二笔交易:

    session_2> update Child set Status = 'I' where AppId = 123 and Status = 'A';
    
    1 row updated.
    
    session_2> insert into child values (123, 'A', sysdate);
    
    1 row created.
    
    session_2> commit;
    
    Commit complete.
    
    session_2> select * from Child;
    
         APPID S INSERTEDAT
    ---------- - -------------------
           123 I 2010-08-16 18:07:17
           123 I 2010-08-16 18:07:23
           123 I 2010-08-16 18:08:08
           123 A 2010-08-16 18:13:51
    

    编辑技术摘自 Thomas Kyte 的 Expert Oracle Database Architecture 第二版,第 23-24 页。 http://www.amazon.com/Expert-Oracle-Database-Architecture-Programming/dp/1430229462/ref=sr_1_2?ie=UTF8&s=books&qid=1282061675&sr=8-2

    编辑 2 我还建议实施 Patrick Merchand 对此问题的回答,以强制执行 AppId 只能有一个活动记录的规则。所以最终的解决方案将分为两部分,这个答案是关于如何以一种获得你想要的方式进行更新,以及帕特里克确保该表符合保护数据完整性的要求。

    【讨论】:

    • 如果我没有父表,这个选择更新技巧仍然有效吗?谢谢!
    • 我不知道如何在没有父表的情况下使其工作。 select for update 只锁定存在的行。因此,只有一张表,您没有任何可锁定的东西来防止多个插入发生在单独的事务中。您可以测试select * from YourTable where AppId = :AppId for update 它可能会起作用,但会比锁定表更昂贵,尤其是随着YourTable 的增长。为此,您可以考虑添加一个父表。
    • 选择 AppId from child where AppId = 123 for update;不是锁住整张桌子吧?
    • @Ding:select AppId from child where AppId = 123 for update 不会锁定表,只会锁定那些匹配 where 子句的行。
    • +1 为清楚的例子。 @Ding & Shannon:如果你没有父表,那么你可以使用 dbms_lock 包来模拟一个。您可以在这篇博文中看到该技术,包括一个警告:rwijk.blogspot.com/2008/07/scalability-of-dbmslockrequest.html
    【解决方案2】:

    如果您想确保对于给定的 id,数据库中的“活动”记录永远不会超过一个,这是一个很酷的方法(信用在这里): http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:1249800833250

    它利用了 Oracle 不存储完全 NULL 索引条目这一事实,并保证一个特定的 id 不能有多个“活动”记录:

    drop table test
    /
    
    create table test (a number(10), b varchar2(10))
    /
    
    CREATE UNIQUE INDEX unq ON test (CASE WHEN b = 'INACTIVE' then NULL ELSE a END)
    /
    

    这些插件工作正常:

    insert into test (a, b) values(1, 'INACTIVE');
    insert into test (a, b) values(1, 'INACTIVE');
    insert into test (a, b) values(1, 'INACTIVE');
    insert into test (a, b) values(1, 'ACTIVE');
    insert into test (a, b) values(2, 'INACTIVE');
    insert into test (a, b) values(2, 'INACTIVE');
    insert into test (a, b) values(2, 'INACTIVE');
    insert into test (a, b) values(2, 'ACTIVE');
    

    这些插入失败:

    insert into test values(1, 'ACTIVE');
    

    ORA-00001:违反了唯一约束 (SAMPLE.UNQ)

    insert into test values(2, 'ACTIVE');
    

    ORA-00001:违反了唯一约束 (SAMPLE.UNQ)

    【讨论】:

    • +1 表示绝对不可能有两个活动记录
    【解决方案3】:

    昨天我创建了一个测试用例来重现所描述的问题。今天发现测试用例有问题。我没看懂问题,所以我相信我昨天给出的答案是不正确的。

    有两个可能的问题:

    1. 发生了commitupdateinsert 之间。

    2. 这只是新的问题 AppIds.

    测试用例:

    创建测试表并插入两行:

    session 1 > create table test (TestId number primary key
      2             , AppId number not null
      3             , Status varchar2(8) not null 
      4                 check (Status in ('inactive', 'active'))
      5  );
    
    Table created.
    
    session 1 > insert into test values (1, 123, 'inactive');
    
    1 row created.
    
    session 1 > insert into test values (2, 123, 'active');
    
    1 row created.
    
    session 1 > commit;
    
    Commit complete.
    

    开始第一笔交易:

    session 1 > update test set status = 'inactive'
      2         where AppId = 123 and status = 'active';
    
    1 row updated.
    
    session 1 > insert into test values (3, 123, 'active');
    
    1 row created.
    

    开始第二次交易:

    session 2 > update test set status = 'inactive'
      2         where AppId = 123 and status = 'active';
    

    现在会话 2 被阻塞,等待获取第 2 行的行锁。会话 2 无法继续,直到会话 1 中的事务提交或回滚。提交会话 1:

    session 1 > commit;
    
    Commit complete.
    

    现在会话 2 已解锁,我们看到:

    1 row updated.
    

    当会话 2 解除阻塞时,更新语句重新启动,查看会话 1 中的更改,并更新第 3 行。

    session 2 > select * from test;
    
        TESTID      APPID STATUS
    ---------- ---------- --------
             1        123 inactive
             2        123 inactive
             3        123 inactive
    

    在会话 2 中完成事务:

    session 2 > insert into test values (4, 123, 'active');
    
    1 row created.
    
    session 2 > commit;
    
    Commit complete.
    

    检查结果(使用会话 1):

    会话 1 > 从测试中选择 *;

        TESTID      APPID STATUS
    ---------- ---------- --------
             1        123 inactive
             2        123 inactive
             3        123 inactive
             4        123 active
    

    两个updates 不相互阻塞的唯一方法是在一个和另一个之间进行提交或回滚。您正在使用的软件堆栈中的某处可能隐藏着隐式提交。我对 .NET 的了解还不够,无法建议跟踪它。

    但是,如果 AppId 对表来说是全新的,则会发生同样的问题。使用 456 的新 AppId 进行测试:

    session 1 > update test set status = 'inactive'
      2         where AppId = 456 and status = 'active';
    
    0 rows updated.
    

    因为没有写入任何行,所以不使用锁。

    session 1 > insert into test values (5, 456, 'active');
    
    1 row created.
    

    为相同的新 AppId 启动第二个事务:

    session 2 > update test set status = 'inactive'
      2          where AppId = 456 and status = 'active';
    
    0 rows updated.
    

    会话 2 看不到第 5 行,因此它不会尝试获取对其的锁定。继续会话 2:

    session 2 > insert into test values (6, 456, 'active');
    
    1 row created.
    
    session 2 > commit;
    
    Commit complete.
    

    提交会话 1 并查看结果:

    session 1 > commit;
    
    Commit complete.
    
    session 1 > select * from test;
    
        TESTID      APPID STATUS
    ---------- ---------- --------
             1        123 inactive
             2        123 inactive
             3        123 inactive
             4        123 active
             5        456 active
             6        456 active
    
    6 rows selected.
    

    要修复,请使用 Patrick Marchand (Oracle transaction isolation) 的基于函数的索引:

    session 1 > delete from test where AppId = 456;
    
    2 rows deleted.
    
    session 1 > create unique index test_u
      2         on test (case when status = 'active' then AppId else null end);
    
    Index created.
    

    开始新 AppId 的第一笔交易:

    session 1 > update test set status = 'inactive'
      2         where AppId = 789 and status = 'active';
    
    0 rows updated.
    
    session 1 > insert into test values (7, 789, 'active');
    
    1 row created.
    

    同样,会话 1 不会对更新进行任何锁定。第 7 行有写锁。开始第二个事务:

    session 2 > update test set status = 'inactive'
      2         where AppId = 789 and status = 'active';
    
    0 rows updated.
    
    session 2 > insert into test values (8, 789, 'active');
    

    同样,会话 2 看不到第 7 行,因此它不会尝试对其进行锁定。 但是插入尝试写入基于函数的索引上的同一插槽,并阻塞会话 1 持有的写锁。会话 2 现在将等待会话 1 到 commit 或 @987654344 @:

    session 1 > commit;
    
    Commit complete.
    

    我们看到的是会话 2:

    insert into test values (8, 789, 'active')
    *
    ERROR at line 1:
    ORA-00001: unique constraint (SCOTT.TEST_U) violated
    

    此时您的客户可以重试整个事务。 (updateinsert。)

    【讨论】:

    • 嗨,Shannon,我已经在我的机器上测试了 select for update,它工作正常。你介意解释一下为什么你之前的答案是错误的吗?感谢您的详细回复!
    • @Ding:因为update语句应该会导致会话阻塞并在AppId上序列化,除非AppId是新的表(或者AppId没有'active'行。如果是新的,那么select for update 将找不到任何要锁定的行。(没有父表,具有该 AppId。)所以它不会添加任何内容。基本上,我无法重现您所描述的情景。
    【解决方案4】:

    这似乎不是真正的 Oracle 问题,而是您的应用程序中的并发问题。不确定这是什么语言;如果是 Java,你可以 synchronise 方法吗?

    【讨论】:

    • 它是 .NET 应用程序,我们有 8 个 Web 服务器,我认为同步不会起作用。
    【解决方案5】:

    您能否将更新推送到队列中(可能是 AQ),以便它们按顺序执行?

    另一个选项可能是锁定有问题的记录(SELECT FOR UPDATE NOWAIT 或 SELECT FOR UPDATE WAIT)

    【讨论】:

      【解决方案6】:

      @Alex 是正确的,这实际上不是 Oracle 问题,而是应用程序问题。

      也许这样的事情可能对你有用:

      将您的 Oracle 事务放入存储过程中,并以这种方式执行:

      BEGIN
        LOOP
          BEGIN
            SELECT * 
              FROM SaleApp
             WHERE appID = 123
               AND status = 'ACTIVE'
               FOR UPDATE NOWAIT;
            EXIT;
          EXCEPTION
            WHEN OTHERS THEN
              IF SQLCODE = -54 THEN
                NULL;
              ELSE
                RAISE error
              END IF;
          END IF;
        END LOOP;
        UPDATE ....
        INSERT ....
        COMMIT;
      END;
      

      这里的想法是第一个获取并锁定当前活动记录的事务完成。任何其他尝试锁定该记录的事务都将在 SELECT FOR UPDATE NOWAIT 上失败,并循环直到成功。

      根据执行典型事务所需的时间,您可能希望在重试选择之前在异常处理程序中休眠。

      【讨论】:

        【解决方案7】:

        我不完全确定,但我认为如果你将两个事务都设置为 SERIALIZABLE,你会在第二个事务中得到一个错误,这样你就会知道出了什么问题。

        【讨论】:

          【解决方案8】:

          "第 3 和第 4 个都处于活动状态 这是错误的。”

          一个简单的唯一索引可以在数据库级别防止这种情况发生。

          create table rec (id number primary key, app_id number, status varchar2(1));
          create unique index rec_uk_ix on rec (app_id, case when status = 'N' then id end);
          insert into rec values (1,123,'N');
          insert into rec values (2,123,'N');
          insert into rec values (3,123,'N');
          insert into rec values (4,123,'Y');
          insert into rec values (5,123,'Y');
          

          唯一索引确保对于任何状态不是“N”的应用程序只能有一条记录。

          显然应用程序必须捕获错误并知道如何处理它(重试或通知用户数据已更改)。

          【讨论】:

          • 我认为这是不对的。所有 N 条记录也会使唯一索引失败...
          • 不,因为索引中的“N”条记录没有 N,它们具有始终唯一的 id(主键)。在 11g 中可以使用虚拟列,在 10g 中需要基于函数的索引。
          猜你喜欢
          • 2014-08-28
          • 1970-01-01
          • 2011-09-30
          • 2015-06-14
          • 1970-01-01
          • 2023-03-25
          • 2015-07-21
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多