Oracle事务隔离

时间:2010-08-16 20:30:30

标签: oracle transactions isolation-level

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

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

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

  1. 记录1,appID 123,无效
  2. 记录2,appID 123,有效
  3. 如果我同时在两个线程中调用此方法SaveApp(),第一个事务(让我们称之为 T1 )将更新现有的两个记录而第二个事务(让我们称之为 T2 )等一下。

    T1 结束后

    ,此表中将有三条记录。但是, T2 不知道新插入的记录, T2 中的更新查询只更新前两个记录,并插入第四个记录。

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

    1. 记录1,appID 123,无效
    2. 记录2,appID 123,无效
    3. 记录3,appID 123,有效
    4. 记录4,appID 123,有效
    5. 您知道任何解决方案都可以解决这个问题吗?我尝试过使用隔离级别序列化,但不起作用。

      谢谢!

8 个答案:

答案 0 :(得分:6)

您是否有另一个表,每个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.

第二节我们现在看到:

     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对此问题的回答,以获得强制执行A​​ppId只能有一条活动记录的规则的约束。因此,最终解决方案将包含两个部分,即如何以获得所需内容的方式进行更新的答案,以及Patrick确保表符合保护数据完整性的要求。

答案 1 :(得分:4)

如果你想确保在db中对于给定的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)

答案 2 :(得分:1)

昨天我创建了一个测试用例来重现所描述的问题。今天我发现测试用例存在缺陷。我不明白这个问题,因此,我相信我昨天给出的答案是错误的。

有两个可能的问题:

  1. 发生commit次 在updateinsert

  2. 之间
  3. 这只是新问题     AppId秒。

  4. 测试案例

    创建测试表并插入两行:

    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> select * from test;

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

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

    但是,如果AppId对于表格是全新的,则会出现同样的问题。使用新的AppId 456进行测试:

    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行,因此它不会尝试对其进行锁定。 BUT 插件尝试写入基于函数的索引上的相同插槽,并阻塞会话1持有的写锁定。会话2现在将等待会话1到commitrollback

    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。)

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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

答案 5 :(得分:0)

@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上失败,并循环直到它们成功。

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

  

“第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”的任何应用。

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