使用 Oracle 和 PL/SQL 插入或更新
我有一个 PL/SQL 函数,它在 Oracle 数据库上执行更新/插入,该数据库维护目标总计并返回现有值和新值之间的差值。
这是我到目前为止的代码:
FUNCTION calcTargetTotal(accountId varchar2, newTotal numeric ) RETURN number is
oldTotal numeric(20,6);
difference numeric(20,6);
begin
difference := 0;
begin
select value into oldTotal
from target_total
WHERE account_id = accountId
for update of value;
if (oldTotal != newTotal) then
update target_total
set value = newTotal
WHERE account_id = accountId
difference := newTotal - oldTotal;
end if;
exception
when NO_DATA_FOUND then
begin
difference := newTotal;
insert into target_total
( account_id, value )
values
( accountId, newTotal );
-- sometimes a race condition occurs and this stmt fails
-- in those cases try to update again
exception
when DUP_VAL_ON_INDEX then
begin
difference := 0;
select value into oldTotal
from target_total
WHERE account_id = accountId
for update of value;
if (oldTotal != newTotal) then
update target_total
set value = newTotal
WHERE account_id = accountId
difference := newTotal - oldTotal;
end if;
end;
end;
end;
return difference
end calcTargetTotal;
这在单元测试中按预期工作,多线程永远不会失败。
然而,当加载到实时系统上时,我们看到此失败,堆栈跟踪如下所示:
ORA-01403: no data found
ORA-00001: unique constraint () violated
ORA-01403: no data found
行号(我已将其删除,因为它们脱离上下文毫无意义)验证第一次更新由于没有数据而失败,插入失败由于唯一性,第二次更新失败且没有数据,这应该是不可能的。
从我在其他线程上读到的内容来看, MERGE 语句也不是原子的,可能会遇到类似的问题。
有谁知道如何防止这种情况发生?
I have a PL/SQL function that performs an update/insert on an Oracle database that maintains a target total and returns the difference between the existing value and the new value.
Here is the code I have so far:
FUNCTION calcTargetTotal(accountId varchar2, newTotal numeric ) RETURN number is
oldTotal numeric(20,6);
difference numeric(20,6);
begin
difference := 0;
begin
select value into oldTotal
from target_total
WHERE account_id = accountId
for update of value;
if (oldTotal != newTotal) then
update target_total
set value = newTotal
WHERE account_id = accountId
difference := newTotal - oldTotal;
end if;
exception
when NO_DATA_FOUND then
begin
difference := newTotal;
insert into target_total
( account_id, value )
values
( accountId, newTotal );
-- sometimes a race condition occurs and this stmt fails
-- in those cases try to update again
exception
when DUP_VAL_ON_INDEX then
begin
difference := 0;
select value into oldTotal
from target_total
WHERE account_id = accountId
for update of value;
if (oldTotal != newTotal) then
update target_total
set value = newTotal
WHERE account_id = accountId
difference := newTotal - oldTotal;
end if;
end;
end;
end;
return difference
end calcTargetTotal;
This works as expected in unit tests with multiple threads never failing.
However when loaded on a live system we have seen this fail with a stack trace looking like this:
ORA-01403: no data found
ORA-00001: unique constraint () violated
ORA-01403: no data found
The line numbers (which I have removed since they are meaningless out of context) verify that the first update fails due to no data, the insert fail due to uniqueness, and the 2nd update is failing with no data, which should be impossible.
From what I have read on other thread a MERGE statement is also not atomic and could suffer similar problems.
Does anyone have any ideas how to prevent this from occurring?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
正如 Oracle 告诉您的那样,您遇到的情况并非不可能。如果另一个进程插入了您尝试插入但尚未提交的密钥,您可以获得所描述的行为。更新不会看到插入的记录,但即使尚未提交插入的行,也禁止尝试将重复值添加到唯一索引。
想到的唯一解决方案是最大限度地减少任何未提交的插入在此表上停留的时间,或者实施某种锁定方案,或者在插入失败时等待其他事务完成。
It's not an impossible situation you're encountering, as Oracle is telling you. You can get the described behavior if another process has inserted the key you're trying to insert but not yet committed. Updates won't see the inserted record, but the attempt to add the duplicate value to the unique index is prohibited even if the inserted row is not committed yet.
The only solutions that come to mind are to minimize the amount of time any uncommitted inserts are hanging around for this table, or to implement some sort of locking scheme, or to wait when your insert fails for the other transaction to complete.
不太同意 DCookie 的观点。
如果会话 A 插入值“blue”(强制为唯一),然后会话 B 插入值“blue”,则会话 B 将等待会话 A 的锁定。如果会话 A 提交,则会话 B 将获得约束违反。如果会话 A 进行回滚,则会话 B 将被允许继续。
会话 A 插入一行并提交它、会话 B 获取约束冲突以及在会话 B 更新该行之前删除该行的范围可能非常小。但我认为这不太可能。
我首先查看 target_total 表上是否只有一个唯一约束。如果不是,您需要非常确定哪个约束导致了违规。还要检查唯一索引和约束。
检查是否存在任何数据类型不匹配或干扰触发器。在选择匹配中,NUMBER(2,0) 可能不等于 1.1 数值,但在插入时,1.1 会被截断为 1.0,可能会触发约束违规。在我的示例中,如果触发器强制使用大写“BLUE”,则选择可能无法匹配“blue”,插入可能无法匹配“BLUE”上的重复键,并且后续插入也无法匹配“蓝色的”。
然后检查变量命名。在 INSERT .... VALUES (标识符) 中,标识符 必须是 PL/SQL 变量。但是,如果 SELECT * FROM table WHERE column = identifier,则 identifier 可能是列名而不是 PL/SQL 变量。如果存在列名或 accountId 函数,则其优先级高于同名的 PL/SQL 变量。为 PL/SQL 变量添加前缀是一个好习惯,以确保永远不会出现此类命名空间冲突。
我唯一的另一个想法是,由于您正在运行多线程,因此线程是否有可能发生冲突。在实时环境中,当线程可能遇到来自其他会话的锁时,这种情况可能更常见。这可能会迫使它们以一种奇怪的方式同步,而这种方式不会在测试中出现。
Don't quite agree with DCookie.
IF session A inserts value "blue" (which is enforced to be unique), and then session B inserts value "blue", session B will wait on the lock from session A. If session A commits, then session B will get the constraint violation. if session A does a rollback, then session B will be allowed to continue.
Potentially, there is a very small scope for session A to insert a row and commit it, session B to get the constraint violation and then the row to be deleted before session B gets to update it. I'd judge that very unlikely though.
I'd first look at whether there is only one unique constraint on the target_total table. If not, you want to be very sure which constraint is causing the violation. Also check for unique indexes as well as constraints.
Check whether there is any datatype mismatch or an interfering trigger. A NUMBER(2,0) might not equal a 1.1 numeric value in a select match, but on insert the 1.1 would get truncated to a 1.0, potentially triggering a constraint violation. In my example, if a trigger had forced an uppercase "BLUE", then the select might fail to match on "blue", the insert might fail on a duplicate key on "BLUE", and the subsequent insert also fails to match on "blue".
Then check for variable naming. In an INSERT .... VALUES (identifier), then identifier must be a PL/SQL variable. However a SELECT * FROM table WHERE column = identifier, then identifier might be a column name not a PL/SQL variable. If there is a column name or a function of accountId, that would take precedence over the PL/SQL variable of the same name. It is a good habit to prefix PL/SQL variables to ensure there is never such a namespace conflict.
My only other idea is that, since you are running multi-threaded, is there any potential for the threads to conflict. This might be more likely in a live environment when threads might hit locks from other sessions. This might force them to synchronise in an odd fashion that doesn't crop up in testing.