我们有一个应用程序允许用户在数据库上执行某些操作(这并不重要),然后使用第三方软件发送通知(它只能使用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 表只读为其他过程)。
任何想法我怎么能实现这个目标?
答案 0 :(得分:2)
使用sp_getapplock
和sp_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
请注意,如果使用此方法,如果作业退出或中断,您可能必须手动解锁通知表。
您还可以通过将日期锁定添加到表中来添加超时,然后您可以定期检查,如果它过去太远,只需解锁即可。