即使在多线程的情况下,使TSQL插入/更新也能正常工作

时间:2011-02-20 20:32:55

标签: sql multithreading tsql

我有一张表用于跟踪客户使用情况。我跟踪用户每天的点击次数,所以我有一个看起来像这样的表:

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)

有更好的方法吗?

此外,我想迁移到多个Web服务器可以为同一个客户调用存储过程的环境。我认为这段代码可能容易受到多线程问题的影响。

谢谢!

3 个答案:

答案 0 :(得分:1)

首先,您可以使用以下方法将当前日期和时间转换为仅日期值:

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

一种解决方案是使用两个语句。但是,为了防止Phantom Read,您需要将语句包装在事务中并将隔离级别设置为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的插入),并在报告端,汇总“每天命中”。

答案 1 :(得分:0)

您有可能发生以下情况:

  1. 一个连接执行if语句表达式,发现当前计数为0
  2. 另一个连接执行if语句表达式,发现当前计数为0
  3. 第一个连接设法插入新行
  4. 第二个连接因异常而崩溃
  5. 此外,您存储完整日期和时间然后使用函数检查您今天是否有现有行看起来很奇怪。如果您需要记录行适用的日期以及添加行的时间,我可能会将其分成两列,以便您可以在第一列中使用索引。这些函数不是sargable,这意味着查询优化器将无法使用索引来查找匹配的行。

    要解决并发问题,我会:

    1. 在执行任何操作之前对表发出锁定
    2. 或者,只是执行插入就像它是第一行一样,每次,并在重复键上处理异常(你有一个包含客户ID +日期的密钥,对吗?)而是更新。这将使每个陈述自己完成。
    3. 第二点在普通编程语言中看起来像这样,你必须为SQL重写:

      try
      {
          Insert ...
      }
      catch (DuplicateKeyException)
      {
          Update ...
      }
      

答案 2 :(得分:0)

你可以这样做。

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。

这与Thomas非合并答案基本相同,但我在更新前进行了插入。

编辑1 在插入语句where not exists部分添加了表提示。需要serializable来防止主键约束违规,并且需要updlock来避免死锁。