4

我有一个 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 语句也不是原子的,可能会遇到类似的问题。

有谁知道如何防止这种情况发生?

4

2 回答 2

1

正如 Oracle 告诉您的那样,您遇到的情况并非不可能。如果另一个进程插入了您尝试插入但尚未提交的密钥,您可以获得所描述的行为。更新不会看到插入的记录,但即使插入的行尚未提交,也禁止尝试将重复值添加到唯一索引。

想到的唯一解决方案是最小化任何未提交的插入在此表中停留的时间,或者实施某种锁定方案,或者在插入失败时等待其他事务完成。

于 2011-03-08T18:40:25.907 回答
1

不太同意 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 ( identifier ) 中,identifier必须是 PL/SQL 变量。但是 SELECT * FROM table WHERE column = identifier,那么identifier可能是列名而不是 PL/SQL 变量。如果存在accountId的列名或函数,则优先于同名的 PL/SQL 变量。为 PL/SQL 变量添加前缀是一个好习惯,以确保永远不会发生这样的命名空间冲突。

我唯一的另一个想法是,由于您正在运行多线程,因此线程是否有可能发生冲突。当线程可能从其他会话中锁定时,这可能更有可能在实时环境中。这可能会迫使他们以一种在测试中不会出现的奇怪方式进行同步。

于 2011-03-09T00:07:51.913 回答