TSQL部分锁定表(但不适用于其他应用程序)

时间:2013-03-06 11:43:52

标签: sql sql-server tsql

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

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

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

我想使用锁定 Notifications 表之类的东西,但该表应该可以被第一行代码中执行的第三方软件读取。换句话说 - 我想只允许一次执行 SendMsg 过程的一个实例(将 Notifications 表只读为其他过程)。
任何想法我怎么能实现这个目标?

3 个答案:

答案 0 :(得分:2)

使用sp_getapplocksp_releaseapplock给自己mutex。有关详细信息,请参阅Application Locks (or Mutexes) in SQL Server 2005。如果您的外部命令足够快,那么让第二个进程等待获取锁定没有任何害处。您还可以删除WAITFOR DELAY,因为所有内容都会受到应用锁定的约束。

答案 1 :(得分: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添加参数。

答案 2 :(得分: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

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

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