Предотвращение проблем параллелизма с целым числом MAX + 1 в SQL Server 2008 ... создание собственного значения IDENTITY - PullRequest
17 голосов
/ 21 января 2012

Мне нужно увеличить целое число в столбце SQL Server 2008.

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

Например,

Customer1  (Order #s 1,2,3,4,5...)
Customer2  (Order #s 1,2,3,4,5...)

По сути, мне нужно будет вручную выполнить функцию SQL identity, поскольку число клиентов не ограничено, и мне нужны счетчики order # для каждого из них.

Мне вполне комфортно делать:

BEGIN TRANSACTION
  SELECT @NewOrderNumber = MAX(OrderNumber)+1 From Orders where CustomerID=@ID
  INSERT INTO ORDERS VALUES (@NewOrderNumber, other order columns here)
COMMIT TRANSACTION

Моя проблема заключается в блокировке и параллелизме и обеспечении уникального значения. Кажется, нам нужно заблокировать с помощью TABLOCKX. Но это база данных большого объема, и я не могу просто блокировать всю таблицу Orders каждый раз, когда мне нужно выполнить процесс SELECT MAX+1 и вставить новую запись заказа.

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

Так какая методология или методика блокировки позволит избежать взаимных блокировок и все же позволяет мне поддерживать уникальные увеличивающиеся номера заказов на клиента?

Ответы [ 7 ]

9 голосов
/ 21 января 2012

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

TABLE CustomerNextOrderNumber
{
    CustomerID id PRIMARY KEY,
    NextOrderNumber int
}

Обновление блокировки при выборе поможет избежать состояния гонки, когда два заказаразмещены одновременно одним и тем же клиентом.

BEGIN TRANSACTION

DECLARE @NextOrderNumber INT

SELECT @NextOrderNumber = NextOrderNumber
FROM  CustomerNextOrderNumber (UPDLOCK)
WHERE CustomerID = @CustomerID

UPDATE CustomerNextOrderNumber
SET   NextOrderNumber = NextOrderNumber + 1
WHERE CustomerID = @CustomerID


... use number here


COMMIT

Подобный, но более простой подход (вдохновленный Иоахимом Исакссоном) блокировка обновления здесь навязывается первым обновлением.

BEGIN TRANSACTION

DECLARE @NextOrderNumber INT

UPDATE CustomerNextOrderNumber
SET   NextOrderNumber = NextOrderNumber + 1
WHERE CustomerID = @CustomerID

SELECT @NextOrderNumber = NextOrderNumber
FROM CustomerNextOrderNUmber
where CustomerID = @CustomerID

...

COMMIT
4 голосов
/ 24 августа 2016

В SQL Server 2005 и более поздних версиях это лучше всего делать атомарно, без каких-либо транзакций или блокировок:

update ORDERS 
set OrderNumber=OrderNumber+1 
output inserted.OrderNumber where CustomerID=@ID
3 голосов
/ 21 января 2012

Вы можете сделать это:

BEGIN TRANSACTION
  SELECT ID
  FROM Customer WITH(ROWLOCK)
  WHERE Customer.ID = @ID

  SELECT @NewOrderNumber = MAX(OrderNumber)+1 From Orders where CustomerID=@ID
  INSERT INTO ORDERS VALUES (@NewOrderNumber, other order columns here)
COMMIT TRANSACTION

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

Если люди вводят заказы для разных клиентов, они друг друга не получат!

ЗдесьВот как это будет работать:

  • Пользователь1 начинает вставлять заказ для Клиента с идентификатором 1000.
  • Пользователь2 пытается вставить заказ для Клиента с идентификатором 1000.
  • Пользователь2 должен ждать, пока Пользователь1 закончит вставку заказа.
  • Пользователь1 вставит заказ, и транзакция будет совершена.
  • Пользователь2 теперь может вставить заказ и гарантированно получит истинный максимальный идентификатор заказаклиент 1000.
2 голосов
/ 21 января 2012

Уровень транзакции по умолчанию, read commit , не защищает вас от фантомных чтений. Фантомное чтение - это когда другой процесс вставляет строку между вашими select и insert:

BEGIN TRANSACTION
SELECT @NewOrderNumber = MAX(OrderNumber)+1 From Orders where CustomerID=@ID
INSERT INTO ORDERS VALUES (@NewOrderNumber, other order columns here)
COMMIT TRANSACTION

Даже на один уровень выше, повторяемое чтение не защитит вас. Только самый высокий уровень изоляции, сериализуемый, защищает от фантомного чтения.

Таким образом, одним из решений является самый высокий уровень изоляции:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
...

Другое решение заключается в использовании подсказок таблицы tablockx, holdlock и updlock , чтобы убедиться, что только ваша транзакция может изменить таблицу. Первый блокирует таблицу, второй сохраняет блокировку до конца транзакции, а третий захватывает блокировку update для выбора, поэтому его не нужно обновлять позже.

SELECT @NewOrderNumber = MAX(OrderNumber)+1 
From Orders with (tablockx, holdlock, updlock)
where CustomerID=@ID

Эти запросы будут быстрыми, если у вас есть индекс на CustomerID, поэтому я не буду слишком беспокоиться о параллелизме, конечно, если у вас меньше 10 заказов в минуту.

0 голосов
/ 22 января 2012
create table TestIds
(customerId int,
nextId int)

insert into TestIds
values(1,1)
insert into TestIds
values(2,1)
insert into TestIds
values(3,1)

go

create proc getNextId(@CustomerId int)
as

declare @NextId int

while (@@ROWCOUNT = 0)
begin
    select @NextId = nextId
    from TestIds
    where customerId = @CustomerId

    update TestIds
    set nextId = nextId + 1
    where customerId = @CustomerId
    and nextId = @NextId

end

select @NextId  
go
0 голосов
/ 21 января 2012

Вы пытаетесь связать два совершенно разных требования.

Даже если у тебя это работает. Что произойдет, если у Клиента А будет удален более ранний заказ, собираетесь ли вы перенумеровать все свои существующие записи, чтобы сохранить их последовательными и начиная с 1. Теперь это будет проблемой блокировки ...

Присвойте записи идентификатор (или, возможно, гид). Когда вы хотите подсчет, запросите его, если вы хотите номер строки (сам никогда не видел смысла), используйте rowno.

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

Время бокового мышления.

Если вы представите

Order Description Date Due
1     Staples     26/1/2012
2     Stapler     1/3/2012
3     Paper Clips 19/1/2012

это не означает (и фактически не должно означать), что ключи заказа равны 1, 2 и 3, они могут быть любыми, если они удовлетворяют требованию уникальности.

0 голосов
/ 21 января 2012

можно было бы создать таблицу с полем IDENTITY для каждого клиента, затем вы можете вставить новую запись в таблицу клиента и извлечь значение из этого.

...