0

我们有一个应用程序允许用户在数据库上做某事(这并不重要),然后使用 3rd 方软件发送通知(它只能使用 select 语句从数据库中读取数据)。
通知由两个 SQL 存储过程处理,第一个将通知添加到Notifications表(可以在发送之前添加许多通知),第二个使用以下代码发送它们:

EXEC master.dbo.xp_cmdshell 'c:\send.cmd';  
WAITFOR DELAY '00:00:05';  
DELETE FROM Notifications;

问题在于第二个过程 - 有时会发生 2 个用户同时触发发送消息,导致将通知表的全部内容发送给收件人两次。

我想使用诸如锁定通知表之类的东西,但是该表应该可以被在第一行代码中执行的第 3 方软件读取。或者换句话说 - 我想一次只允许执行一个SendMsg过程的实例(将Notifications表留给其他过程只读)。
有什么想法我怎么能做到这一点?

4

3 回答 3

2

使用sp_getapplockandsp_releaseapplock给自己一个mutex. 有关更多详细信息,请参阅SQL Server 2005 中的应用程序锁(或互斥锁)。如果您的外部命令足够快,让第二个进程等待获得锁没有害处。您还可以删除您的WAITFOR DELAY,因为一切都将由应用程序锁定管理。

于 2013-03-06T18:14:03.560 回答
0

您需要在通知表中添加一个附加字段,指示它是哪一批通知。

create table notifications(
   batchID uniqueidentifier,
   text nvarchar(max)

)

因此AddNotification将采用一个额外的 GUID 参数,即批次。然后你的发送过程应该这样做:

declare @cmd nvarchar(max)
set @cmd = 'c:\send.cmd' + ' ' + cast(@batchID as varchar(36))

EXEC master.dbo.xp_cmdshell @cmd;  
--- WAITFOR DELAY '00:00:05';  
-- What are you waiting for? xp_cmdshell waits already.
-- You can also induce CMD.exe to wait for windows programs using the START /WAIT option
DELETE FROM Notifications where batchID = @batchID;

显然 send.cmd 需要修改为采用相同的 GUID 作为参数。

如果您无法向存储过程添加参数,则使用 SPID ( @@spid) 可能会起作用,前提是您可以确保所有存储过程都使用相同的底层连接(来自连接池)。尽管如此,您仍然必须添加一个参数send.cmd

于 2013-03-06T12:18:06.907 回答
0

编辑:请参阅 David T Macknet 的答案,这对于 MSSQL 2005 更高版本要好得多。不过,这种技术在其他 DBMS 或更早版本的 SQL Server 上可能仍然有用。

您可以使用第二个表来保存该表是否被锁定的事实。

create table Notifications_Lock(
ix int primary key,
fLocked bit,
constraint Notifications_Lock_SingleRow check (ix = 1)
)

insert Notifications_Lock values( 1, 0, null)

go

create proc NotificationsLockTry
as
begin
    update Notifications_Lock set fLocked = 1 
    from Notifications_Lock with (TABLOCKX)
    where fLocked = 0
    return @@rowcount
end
go
create proc NotificationsLockTimeout( @waitSeconds int)
as
begin
    set @waitSeconds = isnull(@waitSeconds, 0)

    declare @dtWaitTill datetime
    set @dtWaitTill = dateadd(second, @waitSeconds, getutcdate())
    declare @fLocked int

    update Notifications_Lock set fLocked = 1 
    from Notifications_Lock with (TABLOCKX)
    where fLocked = 0
    set @fLocked = @@rowcount

    if @fLocked > 0  return @fLocked

    while @fLocked = 0 And @dtWaitTill > getutcdate()
    begin
        waitfor delay '00:00:01'
        update Notifications_Lock set fLocked = 1 
        from Notifications_Lock with (TABLOCKX)
        where fLocked = 0
        set @fLocked = @@rowcount
        if @fLocked > 0 return @fLocked
    end
    return @fLocked
end
go
create proc NotificationsUnlock
as
begin 
    update Notifications_Lock set fLocked = 0, dtLocked = null
    from Notifications_Lock with (TABLOCKX)
    where fLocked = 1
    return @@rowcount
end

示例用法:

declare @fLocked int
-- Wait up to 5 minutes for a lock
exec @fLocked = NotificationsLockTry 300
if @fLocked = 0 
begin
    raiserror('Unable to lock notifications table', 11,11)
    return
end
-- Locked OK
-- INSERT NOTIFICATIONS HERE
-- CALL Send.cmd HERE
exec NotificationsUnlock

return

请注意,如果使用此方法,如果作业退出或中断,您可能必须手动解锁通知表。

您还可以通过将锁定日期添加到表中来添加超时,然后您可以定期检查它,如果它过去太久,只需解锁它。

于 2013-03-06T14:28:14.030 回答