Устранение проблем concurrency с целым числом MAX + 1 в SQL Server 2008... создание собственного значения IDENTITY

Мне нужно увеличить число в столбце 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 [email protected]
  INSERT INTO ORDERS VALUES (@NewOrderNumber, other order columns here)
COMMIT TRANSACTION

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

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

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

Ответ 1

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

update ORDERS 
set OrderNumber=OrderNumber+1 
output inserted.OrderNumber where [email protected]

Ответ 4

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

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

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

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

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
...

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

SELECT @NewOrderNumber = MAX(OrderNumber)+1 
From Orders with (tablockx, holdlock, updlock)
where [email protected]

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

Ответ 5

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

Ответ 6

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

Даже если у вас это работает. Что произойдет, если клиент A удалит заказ эльфира, вы собираетесь перенумеровать все свои существующие записи, чтобы сохранить их последовательно и начиная с 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, они могут быть любыми, пока они выполняют требование уникальности.

Ответ 7

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