Параллелизм базы данных, необходимый при добавлении строк - передовой опыт? - PullRequest
2 голосов
/ 15 июня 2009

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

Вот упрощенная визуализация:

A1
A2
A3
B1
B2

Используя приведенную выше визуализацию, веб-страница загружает самое высокое значение «B», которое равно «2». Затем через некоторое время он хочет вставить B3, следующую запись в серии. Однако необходимо убедиться, что кто-то другой не сделал то же самое.

Как я уже говорил, я знаю, что могу прочитать ожидаемое значение в транзакции, или я могу заблокировать таблицу, или, возможно, даже последнюю строку. Я спрашиваю, есть ли рекомендуемая стратегия.

Ответы [ 10 ]

1 голос
/ 15 июня 2009

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

Я бы повторил это, возможно, чтобы быть более управляемым и однозначным. Вы можете просто вставить запись, подтверждающую, что машина находится в состоянии, с отметкой времени?

Получение текущего шага из предыдущей информации (которая сама по себе может быть недостаточно известна) представляется рискованной, особенно если это простой итерационный расчет, который может происходить от 0 до n раз в зависимости от обстоятельств.

OTOH, если это наблюдение за фактическим состоянием с отметкой времени, то оно само исправляется (не имеет значения, в каком состоянии вы думали, что оно было раньше), и множественные утверждения не вызывают проблем.

Можете ли вы реконструировать логику таким образом, основываясь на существующих формах (или, может быть, небольшая модификация формы, сетевой конфигурации или чего-то еще)? Есть ли пользователь, IP-адрес и т. Д., Связанные с данным шагом подмножества шагов? Существуют ли связанные транзакции, которые действительны только в том случае, если это шаг или подмножество шагов?

1 голос
/ 15 июня 2009

Это выглядит как классический случай необходимости оператора, которого я всегда хочу: «убедиться, что кортеж, удовлетворяющий этим условиям, существует, и дайте мне ключ».

В моем случае обычно это просто: «У меня есть номер этой кредитной карты и дата истечения срока действия, какой для нее ключ?» На самом деле мне все равно, находится ли он уже в БД или нет (на самом деле приложение не должно быть в состоянии сказать, в целях безопасности), я просто хочу его идентификатор, если он есть, или я хочу, чтобы он был создан, если это не так, и получите новый идентификатор для этого создания.

Насколько я могу судить, с текущей технологией СУБД вам нужно заблокировать таблицу, потому что вы должны принять решение, вставлять или нет, основываясь на том, что уже есть. Однако я хотел бы найти лучшее решение для этого.

1 голос
/ 15 июня 2009

Смотрите эту запись в моем блоге о том, как сделать это, используя рекурсивные CTE и один IDENTITY:

Обновление:

Если проблема состоит в том, чтобы собрать оборудование для следующего шага, то вам, вероятно, лучше использовать абсолютное значение вместо относительного.

Запомните предыдущее значение шага в переменной (на самой странице или на стороне сервера) и просто обновите его новым значением переменной.

Вместо этого:

UPDATE  mytable
SET     step = step + 1

используйте это:

SET @nextstep = 2
UPDATE  mytable
SET     step = @nextstep

Вы также можете добавить автоинкрементное поле last_update в столбец, чтобы убедиться, что вы обновляете столбец, который не обновлялся после загрузки вашей страницы:

SELECT  last_update
INTO    @lastupdate
FROM    mytable
WHERE   item_id = @id

UPDATE  mytable
SET     step = @nextstep
WHERE   item_id = @id
        AND last_update = @lastupdate

Обновление 2 :

Если вы используете связанный список состояний (т.е. вы не обновляете, а вставляете новые состояния), просто отметьте столбец IDENTITY и вставьте ID предыдущего состояния:

item_id  step_id  prev_step_id
1        10232     0
1        12123     10232

, сделайте step_id и prev_step_id уникальными и запросите так:

WITH    q (item_id, step_id, step_no) AS
        (
        SELECT  item_id, step_id, 1
        FROM    mytable
        WHERE   item_id = 1
                AND prev_step_id = 0
        UNION ALL
        SELECT  q.item_id, m.step_id, q.step_no + 1
        FROM    q
        JOIN    mytable m
        ON      m.item_id = q.item_id
                m.prev_step_id = q.step_id
        )
SELECT  *
FROM    q

Если два человека хотят вставить две записи, то ограничение UNIQUE на prev_step_id сработает, и последняя вставка завершится неудачей.

0 голосов
/ 16 июня 2009

Рекомендуемая стратегия SQL, безусловно, должна использовать SELECT FOR UPDATE. Я удивлен, что никто не упомянул об этом.

SELECT id FROM tasks WHERE id = max(id) FOR UPDATE OF tasks;

SELECT FOR UPDATE блокирует только то, что вам нужно, так что это намного проще, чем ручная блокировка.

0 голосов
/ 15 июня 2009

Правильная стратегия зависит от того, какие именно действия происходят между чтением B2 и вставкой B3. Следующие пункты являются скорее теоретическими, а не практическими примерами T-SQL, но я понимаю, что в этом смысл вашего вопроса.

  • Если это дешевое действие с одноразовым результатом, то вы можете позволить каждой транзакции сделать это, тогда первая, которая вставляет B3, будет успешной, а остальные потерпят неудачу с нарушением дублирующего ключа (ограничение уникальности), восстановятся после исключения и возобновят работу, как если бы ничего не произошло.
  • Относительно дорогая операция с одноразовым результатом, но вряд ли произойдет одновременно. То же, что и выше, вы будете наказаны за удаление результата «дорогой» операции, но это случается редко.
  • Не одноразовый результат, который можно откатить (можно встроить в транзакцию или зарегистрировать в DTC) и вряд ли произойдет одновременно. То же, что и выше, но зарегистрируйте «операцию» в своей транзакции (локально или по коду DTC), при конфликте вы откатываетесь и попробуйте снова с новым значением последовательности Bs.
  • Результат не подлежит уничтожению (результат «операций» нельзя игнорировать, его необходимо записать, например, вы записали финансовую транзакцию). В этом случае вы должны предотвратить параллелизм, и блокировка - это путь. Блокировки таблиц всегда избыточны, вам, вероятно, следует искать UPDLOCK в поиске после 'B2'. К сожалению, уровни изоляции транзакций здесь не помогают (на всех уровнях два потока могут одновременно прочитать 'B2' и погрузиться в него, что, как мы надеемся, приведет к тупику во время вставки). Если «операция» между чтением B2 и вставкой B3 является чем-то таким сложным, как возвращение пользователю HTML и ожидание следующего POST, то вы, вероятно, не сможете позволить себе оставить такие долгоживущие U-блокировки на реальных данных и лучшие способ состоит в том, чтобы создать схему блокировки приложения, используя sp_getapplock .
0 голосов
/ 15 июня 2009

Просто оберните это в то же утверждение. Первая вставка нового значения (2) будет успешной, вторая добавит ноль строк.

create table t1 (i int)
insert t1 values (1)

insert t1 (i)
    select 2 where exists (select max(i) from t1 having max(i) = 1)

insert t1 (i)
    select 2 where exists (select max(i) from t1 having max(i) = 1)
0 голосов
/ 15 июня 2009

Я знаю, что приведенный вами пример упрощен, но для удовлетворения этих требований вы можете сделать следующее.

Поскольку INSERT / SELECT является одним оператором, он неявно присутствует в транзакции. Если вам нужно сделать что-то, что вы не можете выразить реляционно, вам нужно заключить это в явную транзакцию.

Первичный ключ гарантирует отсутствие проблем параллелизма за пределами заданной изоляции транзакции по умолчанию.

CREATE TABLE Sequence
(
    [Name] char(1),
    [Seq] int
    PRIMARY KEY (Name, Seq)
)
GO
CREATE PROCEDURE Sequence_Insert
(
    @name char(1)
)
AS
INSERT INTO Sequence(Name, Seq)
SELECT 
    @Name,
    COALESCE(MAX(Seq),0) + 1
FROM
    Sequence
WHERE
    Name = @Name
GO
exec Sequence_Insert 'A'
exec Sequence_Insert 'A'
exec Sequence_Insert 'B'
exec Sequence_Insert 'A'
exec Sequence_Insert 'C'
GO
SELECT * FROM Sequence
0 голосов
/ 15 июня 2009

обычный способ сделать это состоит в том, чтобы иметь столбец типа rowversion и сверять значение строки со значением, поступающим от клиента. если строка в таблице была обновлена, числа не совпадут. Индексируйте столбец rowversion, и он будет летать.

0 голосов
/ 15 июня 2009
UPDATE yourtable
  SET  location = 'B3'
 WHERE primary-key = 1231421
   AND location    = 'B2'

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

Хорошо, учитывая новое требование к строке:

INSERT INTO yourtable ( item, location ) VALUES( 123, 'B3' )
 WHERE NOT EXISTS( SELECT * FROM yourtable WHERE item = 123 AND location = B3 )

пусть база данных сделает всю работу за вас.

0 голосов
/ 15 июня 2009

Это идеальный случай для использования очереди. Попробуйте Message Broker.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...