使用Oracle和PL / SQL插入或更新

时间:2011-03-08 18:25:53

标签: sql oracle plsql upsert ora-00001

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

有没有人有任何想法如何防止这种情况发生?

2 个答案:

答案 0 :(得分:1)

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

我们想到的唯一解决方案是尽量减少任何未提交的插件为此表挂起的时间,或实现某种锁定方案,或等待插入失败以完成其他事务。

答案 1 :(得分:1)

不同意DCookie。

如果会话A插入值“蓝色”(强制为唯一),然后会话B插入值“蓝色”,会话B将等待来自会话A的锁定。如果会话A提交,则会话B将得到约束违规。如果会话A进行回滚,则会话B将被允许继续。

可能,会话A有一个非常小的范围,用于插入行并提交它,会话B用于获取约束违规,然后在会话B更新之前删除要删除的行。我认为这不太可能。

我首先看一下target_total表上是否只有一个唯一约束。如果没有,您希望非常确定哪个约束导致违规。还要检查唯一索引和约束。

检查是否存在任何数据类型不匹配或干扰触发。在选择匹配中,NUMBER(2,0)可能不等于1.1数值,但在插入时,1.1将被截断为1.0,可能会触发约束违规。在我的示例中,如果触发器强制大写“BLUE”,则select可能无法匹配“blue”,插入可能会在“BLUE”上的重复键上失败,并且后续插入也无法匹配“蓝色”。

然后检查变量命名。在INSERT .... VALUES(标识符)中,标识符必须是PL / SQL变量。但是,SELECT * FROM表WHERE列= 标识符,则标识符可能是列名而不是PL / SQL变量。如果存在列名称或 accountId 的函数,则优先于同名的PL / SQL变量。为PL / SQL变量添加前缀是一个好习惯,以确保永远不会发生这样的命名空间冲突。

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