我每小时在Azure的Web作业中运行一个后台任务。有时(似乎超过50%的时间,该代码会在此特定代码段上窒息(出现死锁错误):
foreach (var ownerToProcess in activeOwnersWithMessageArchiving)
{
foreach (var extension in extensions)
{
using (var db = new SqlConnection(connectionString))
{
db.Execute(@"
UPDATE T_MESSAGESTARTER
SET Started=@started,Completed=NULL
WHERE OwnerId=@ownerId AND ExtensionId=@extensionId;
if @@ROWCOUNT=0
INSERT INTO T_MESSAGESTARTER (OwnerId,ExtensionId,Started)
VALUES (@ownerId,@extensionId,@started)
", new { ownerId = ownerToProcess, extensionId = extension, started = DateTimeOffset.Now });
}
}
}
这是一个简单的更新/插入语句。我“相信”我也在使用行级阻塞。这不在事务内。另外,顶层大约有60个ownerToProcess
项目。在内循环中,每个循环中都有5-60个extension
项目(在上面的代码中)。这样,每次运行该SQL语句大约需要执行4000次。每个@owner
/ @extension
组合(在WHERE子句中)都是唯一的。
有时它会一直运行到没有错误。但是有时我会在执行SQL语句之一时遇到死锁错误。是什么原因造成的?是因为我在SQL语句中具有UPDATE/INSERT
结构?还是Dapper会做些有趣的事情?
另外要注意的是:所讨论的T_MESSAGESTARTER
表没有主键。这可能导致此问题吗?
不必是PK,但ownerId和extensionId上的复合唯一索引(理想情况下是集群的)将优化更新查询。通过触摸最少的数据量,可以缓解死锁(取决于所涉及的过程)。
即使实际未更新任何行(当行不存在时),更新也会锁定“表”。根据并发性,以下最有可能是安全的(如果两个进程永远不会处理相同的所有者和扩展名,那么它将起作用)
IF EXISTS(SELECT ... FROM T_MESSAGESTARTER WHERE OwnerId=@ownerId....)
BEGIN
UPDATE T_MESSAGESTARTER
SET Started=@started,Completed=NULL
WHERE OwnerId=@ownerId AND ExtensionId=@extensionId;
END
ELSE
INSERT INTO T_MESSAGESTARTER (OwnerId,ExtensionId,Started)
VALUES (@ownerId,@extensionId,@started)
END
无需在两次foreach迭代中打开连接。
using (var db = new SqlConnection(connectionString))
{
foreach (var ownerToProcess in activeOwnersWithMessageArchiving)
{
foreach (var extension in extensions)
{
db.Execute(@"
UPDATE T_MESSAGESTARTER
SET Started=@started,Completed=NULL
WHERE OwnerId=@ownerId AND ExtensionId=@extensionId;
if @@ROWCOUNT=0
INSERT INTO T_MESSAGESTARTER (OwnerId,ExtensionId,Started)
VALUES (@ownerId,@extensionId,@started)
", new { ownerId = ownerToProcess, extensionId = extension, started = DateTimeOffset.Now });
}
}
}
FYI,也存在MERGE语句。这是标准SQL,也可以进行upserts。