Вставка или обновление с использованием Oracle и PL / SQL - PullRequest
4 голосов
/ 08 марта 2011

У меня есть функция 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  

Номера строк (которые я удалил, поскольку они бессмысленны вне контекста) подтверждают, что первое обновление завершается неудачно из-за отсутствия данных, вставка завершается неудачно из-за уникальности, а 2-е обновление не выполняется без данных, что должнобыть невозможным.

Из того, что я прочитал в другом потоке, оператор MERGE также не является атомарным и может страдать от подобных проблем.

У кого-нибудь есть идеи, как этого избежать?

Ответы [ 2 ]

1 голос
/ 09 марта 2011

Не совсем согласен с DCookie.

ЕСЛИ сеанс А вставляет значение «синий» (которое принудительно является уникальным), а затем сеанс В вставляет значение «синий», сеанс Б будет ожидать наблокировка от сеанса A. Если сеанс A фиксируется, тогда сеанс B получит нарушение ограничения.если сеанс A выполняет откат, то сеансу B будет разрешено продолжить.

Потенциально, для сеанса A существует очень небольшая область для вставки строки и ее фиксации, сеанса B для получения нарушения ограничения, а затемстрока, которая будет удалена до того, как сессия B сможет обновить ее.Я бы сказал, что это очень маловероятно.

Сначала я посмотрю, существует ли только одно уникальное ограничение для таблицы target_total.Если нет, вы хотите быть очень уверенным, какое ограничение вызывает нарушение.Также проверьте наличие уникальных индексов и ограничений.

Проверьте, есть ли какое-либо несоответствие типа данных или мешающий триггер.NUMBER (2,0) может не совпадать с числовым значением 1.1 в сопоставлении выбора, но при вставке 1.1 будет усечено до 1,0, что может привести к нарушению ограничения.В моем примере, если для триггера принудительно задана заглавная буква «СИНИЙ», тогда выбор может не совпадать с «синим», вставка может завершиться ошибкой по дублирующему ключу «СИНИЙ», а последующая вставка также не будет соответствовать «синий ".

Затем проверьте имя переменной.В INSERT .... VALUES ( идентификатор ), тогда идентификатор должен быть переменной PL / SQL.Однако столбец SELECT * FROM таблицы WHERE = идентификатор , тогда идентификатор может быть именем столбца, а не переменной PL / SQL.Если есть имя столбца или функция accountId , это будет иметь приоритет над переменной PL / SQL с тем же именем.Хорошей привычкой является префикс переменных PL / SQL, чтобы никогда не возникало такого конфликта пространства имен.

Моя единственная другая идея заключается в том, что, поскольку вы работаете в многопоточном режиме, есть ли потенциал для потоков, чтобыконфликт.Это может быть более вероятным в реальной среде, когда потоки могут блокировать блокировки из других сеансов.Это может заставить их синхронизироваться странным образом, который не возникает при тестировании.

1 голос
/ 08 марта 2011

Это не невозможная ситуация, с которой вы сталкиваетесь, как говорит вам Oracle. Вы можете получить описанное поведение, если другой процесс вставил ключ, который вы пытаетесь вставить, но еще не зафиксировал. Обновления не увидят вставленную запись, но попытка добавить дублирующее значение в уникальный индекс запрещена, даже если вставленная строка еще не зафиксирована.

Единственные решения, которые приходят на ум, - это минимизировать количество времени, в течение которого любые незавершенные вставки зависают для этой таблицы, или реализовывать какую-либо схему блокировки, или ждать, когда вставка завершится неудачно, для завершения другой транзакции.

...