我有一个方法SaveApp(),它将停用现有记录并插入一个新记录。
void SaveApp(int appID)
{
begin transaction;
update;
insert;
commit transaction;
}
假设在数据库表SalesApp中,我有2条appID等于123的记录;
如果我同时在两个线程中调用此方法SaveApp()
,第一个事务(让我们称之为 T1 )将更新现有的两个记录而第二个事务(让我们称之为 T2 )等一下。
,此表中将有三条记录。但是, T2 不知道新插入的记录, T2 中的更新查询只更新前两个记录,并插入第四个记录。
在这两个方法调用之后,在数据库中,我们现在将有4条记录,第3条和第4条都是活动的,这是错误的。
您知道任何解决方案都可以解决这个问题吗?我尝试过使用隔离级别序列化,但不起作用。
谢谢!
答案 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对此问题的回答,以获得强制执行AppId只能有一条活动记录的规则的约束。因此,最终解决方案将包含两个部分,即如何以获得所需内容的方式进行更新的答案,以及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)
昨天我创建了一个测试用例来重现所描述的问题。今天我发现测试用例存在缺陷。我不明白这个问题,因此,我相信我昨天给出的答案是错误的。
有两个可能的问题:
发生commit
次
在update
和insert
。
这只是新问题
AppId
秒。
测试案例
创建测试表并插入两行:
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到commit
或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
此时您的客户可以重试整个交易。 (update
和insert
。)
答案 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”的任何应用。
显然,应用程序必须捕获错误并知道如何处理它(重新尝试或通知用户数据已更改)。