Обеспечение вставки / обновления TSQL даже в многопоточных - PullRequest
2 голосов
/ 20 февраля 2011

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

CustID (uniqueidentifier, not null)

UseDate (smalldatetime, not null)

NumHits (smallint, not null)

Я использую этот SQL в хранимой процедуре для вставки строки на сегодня (при необходимости) или для увеличения счетчика на сегодня:

declare @today datetime
set @today = getdate()

/* Try to bump it by one if an entry for today exists */
if (
    select count(*) from CustomerUsage
    where CustID = @cust_guid and year(UseDate) = year(@today) and month(UseDate) = month(@today) and day(UseDate) = day(@today)
    ) = 0

    insert into CustomerUsage (CustID, UseDate, NumHits) values (@cust_guid, getdate(), 1)

else
    update CustomerUsage set NumHits = NumHits + 1 
    where CustID = @cust_guid and year(UseDate) = year(@today) and month(UseDate) = month(@today) and day(UseDate) = day(@today)

Есть ли лучший способ сделать это?

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

Спасибо!

Ответы [ 3 ]

1 голос
/ 21 февраля 2011

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

DateAdd(d, DateDiff(d, 0, CURRENT_TIMESTAMP), 0)

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

Declare @Today datetime
Set @Today = DateAdd(d, DateDiff(d, 0, CURRENT_TIMESTAMP), 0)

Set Transaction Isolation Level Serializable;
Set Xact_Abort On;

Begin Tran;

Update CustomerUsage 
Set NumHits = NumHits + 1 
Where CustID = @cust_guid 
    And UseDate >= @Today
    And UseDate < DateAdd(d, 1, @Today)

Insert CustomerUsage( CustId, UseDate, NumHits )
Select @CustId, CURRENT_TIMESTAMP, 1
From ( Select 1 As Value ) As Z
Where Not Exists    (
                    Select 1
                    From Customer_Usage
                    Where CustID = @cust_guid 
                        And UseDate >= @Today
                        And UseDate < DateAdd(d, 1, @Today)
                    )

Commit Tran;

Поскольку каждый оператор сам по себе является транзакцией, вы избежите проблем с одновременными вызовами. Если вы используете SQL Server 2008, вы можете использовать тип данных Date и оператор Merge для достижения того же:

Declare @Today date
Set @Today = Cast( CURRENT_TIMESTAMP As date )

Merge CustomerUsage As target
Using ( 
        Select CustId, UseDate
        From CustomerUsage
        Where CustID = @cust_guid 
            And UseDate >= @Today
            And UseDate < DateAdd(d, 1, @Today)
        Union All
        Select @cust_guid, CURRENT_TIMESTAMP 
        From ( Select 1 As Value ) As Z
        Where Not Exists    (
                            Select 1
                            From CustomerUsage
                            Where CustID = @cust_guid 
                                And UseDate >= @Today
                                And UseDate < DateAdd(d, 1, @Today)
                            )
        ) As source
    On source.CustID = target.CustID
        And source.UseDate = target.UseDate
When Matched Then 
    Update Set NumHits = NumHits + 1
When Not Matched Then 
    Insert ( CustId, UseDate, NumHits )
    Values( source.CustId, source.UseDate, 1 )

Окончательное добавление

Хотя я понимаю, что ответ был выбран, мне приходит в голову, что есть лучшее решение. Нет необходимости обновлять счетчик посещений в день. Просто сделайте вставку, которая регистрирует попадание (то есть только вставку без отслеживания NumHits), и на стороне отчетности сворачивайте «попадания в день».

0 голосов
/ 21 февраля 2011

Вы можете сделать это следующим образом.

declare @Today smalldatetime = dateadd(d, datediff(d, 0, getdate()), 0)
declare @Tomorrow smalldatetime = dateadd(d, 1, @Today)

insert into CustomerUsage(CustId, UseDate, NumHits )
select Data.CustID, Data.UseDate, Data.NumHits
from (select
        @cust_guid as CustID,
        getdate() as UseDate,
        0 as NumHits) as Data
where not exists (select *
                  from CustomerUsage with (updlock, serializable)
                  where
                    CustID = @cust_guid and
                    UseDate >= @Today and
                    UseDate < @Tomorrow)        


update CustomerUsage 
set NumHits = NumHits + 1 
where
  CustID = @cust_guid and
  UseDate >= @Today and
  UseDate < @Tomorrow

Сначала вставьте строку с NumHits = 0.Вставка проверяет, существует ли уже строка для этого CustID на этом UseDate и вставляет только при необходимости.

После вставки всегда увеличивается NumHits на 1.

Этов основном та же идея, что и у Томаса без ответа слияния, но я делаю вставку перед обновлением.

Редактировать 1 Добавлены табличные подсказки в оператор вставки where not exists часть.serializable необходим для предотвращения нарушения ограничения первичного ключа, а updlock необходим для предотвращения взаимных блокировок.

0 голосов
/ 20 февраля 2011

У вас есть вероятность следующего сценария:

  1. Одно соединение выполняет выражение оператора if, обнаруживая, что текущий счетчик равен 0
  2. Другое соединение выполняет ifвыражение, обнаружив, что текущий счетчик равен 0
  3. Первому соединению удается вставить новую строку
  4. Второе соединение прерывается с исключением

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

Чтобы решить проблему параллелизма, я бы либо:

  1. Issueзаблокировать таблицу перед выполнением чего-либо
  2. или просто выполнить вставку, как если бы это была первая строка, каждый раз и обработать исключение для дублирующегося ключа (у вас есть ключ, которыйвключает в себя идентификатор клиента + дата, верно?) путем обновления вместо.Это сделало бы каждое утверждение завершенным само по себе.

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

try
{
    Insert ...
}
catch (DuplicateKeyException)
{
    Update ...
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...