5

我有一个方法 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,活动

你知道任何解决方案可以解决这个问题吗?我尝试使用隔离级别可序列化,但它不起作用。

谢谢!

4

8 回答 8

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.

我们现在看到的第 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 只能有一个活动记录的规则。所以最终的解决方案将有两个部分,这个答案是关于如何以一种获得你想要的方式进行更新,以及帕特里克确保该表符合保护数据完整性的要求。

于 2010-08-16T22:15:44.643 回答
4

如果您想确保在给定 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)

于 2010-08-17T00:07:09.327 回答
1

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

有两个可能的问题:

  1. commit之间发生了一件事情。updateinsert

  2. 这只是 new 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

两个 s 不相互阻塞的唯一方法update是在一个和另一个之间进行提交或回滚。您正在使用的软件堆栈中的某处可能隐藏着隐式提交。我对 .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 事务隔离):

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 到commitor rollback

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

于 2010-08-17T20:05:28.943 回答
0

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

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

于 2010-08-16T21:14:55.680 回答
0

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

于 2010-08-16T21:16:18.923 回答
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 上失败,并循环直到它们成功。

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

于 2010-08-16T21:31:31.217 回答
0

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

于 2010-08-16T21:43:48.133 回答
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”的应用程序只能有一条记录。

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

于 2010-08-16T23:02:58.690 回答