SQL 服务器隔离级别和 Table 锁定
SQL Server Isolation Level And Table Locking
假设我在 SQL 服务器中有一个 table 作为需要处理的项目的队列。像这样:
Id (bigint)
BatchGuid (guid)
BatchProcessed (bit)
...
...以及其他一些描述需要处理的项目的列,等等。因此有许多 运行 消费者根据需要向此 table 添加记录以表明项目需要处理。
现在假设我有一份工作负责从这个 table 中获取一批项目并进行处理。假设我们想让它一次处理 10 个。现在还假设此作业可以同时拥有多个实例 运行,因此它正在同时访问 table(以及可能向队列中添加新记录的任何其他消费者)。
我正打算做这样的事情:
using(var tx = new Transaction(Isolation.Serializable))
{
var batchGuid = //newGuid
executeSql("update top(10) [QUeueTable] set [BatchGuid] = batchGuid where [BatchGuid] is null");
var itemsToProcess = executeSql("select * from [QueueTable] where [BatchGuid] = batchGuid");
tx.Commit()
}
所以基本上我要做的是启动一个可序列化的事务,用特定的 GUID 标记 10 个项目,然后获取这 10 个项目,然后提交。
这是可行的策略吗?我相信 serializable 的隔离级别基本上会锁定整个 table 以防止 read/write 直到事务完成 - 这是正确的吗?基本上,事务将阻止 table 上的所有其他 read/write 操作?我相信这就是我在这种情况下想要的,因为我不想读取脏数据并且我不希望并发 运行 作业在标记要处理的批次 10 时相互踩踏。
任何关于我是否走在正确轨道上的见解都将不胜感激。如果有更好的方法来实现这一点,我也欢迎其他选择。
如果使用 OUTPUT
子句,您可以在一条语句中执行此操作:
UPDATE TOP (10) [QueueTable]
OUTPUT inserted.*
SET [BatchGuid] = batchGuid
WHERE [BatchGuid] IS NULL;
或更具体地说:
var itemsToProcess = executeSql("update top(10) [QUeueTable] output inserted.* set [BatchGuid] = batchGuid where [BatchGuid] is null");
我想这是个人偏好,但我从来都不是 UPDATE TOP(n)
语法的粉丝,因为你不能指定 ORDER BY
,而且在大多数情况下指定 top 时,你想要指定一个顺序,我更喜欢使用类似的东西:
UPDATE q
OUTPTUT inserted.*
SET [BatchGuid] = batchGuid
FROM ( SELECT TOP (10) *
FROM dbo.QueueTable
WHERE BatchGuid IS NULL
ORDER BY ID
) AS q
附录
作为对评论的回应,我不认为存在竞争条件的任何可能性,但我不是 100% 确定。我之所以不相信这一点,是因为虽然查询读取为 SELECT,并且是 UPDATE,但它是语法糖,它只是一个更新,并且使用完全相同的计划,并且与顶级查询一样锁定。但是,由于我不确定我决定测试:
首先,我在临时数据库中设置了一个示例 table,并设置了一个日志记录 table 来记录更新的 ID
USE TempDB;
GO
CREATE TABLE dbo.T (ID BIGINT NOT NULL IDENTITY PRIMARY KEY, Col UNIQUEIDENTIFIER NULL);
INSERT dbo.T (Col)
SELECT TOP 1000000 NULL
FROM sys.all_objects a, sys.all_objects b;
CREATE TABLE dbo.T2 (ID BIGINT NOT NULL PRIMARY KEY);
然后在 10 个不同的 SSMS windows 我 运行 这个:
WHILE 1 = 1
BEGIN
DECLARE @ID UNIQUEIDENTIFIER = NEWID();
UPDATE T
SET Col = @ID
OUTPUT inserted.ID INTO dbo.T2 (ID)
FROM ( SELECT TOP 10 *
FROM dbo.T
WHERE Col IS NULL
ORDER BY ID
) t;
IF @@ROWCOUNT = 0
RETURN;
END
在我停止所有 10 个线程之前,整个过程 运行 更新了 ~500,000 行 20 分钟。由于在插入到 T2 时两次更新同一行会抛出错误并作为主键违规,并且需要停止所有 10 个线程,这表明不存在竞争条件,为了确认这一点,我 运行 以下内容:
SELECT Col, COUNT(*)
FROM dbo.T
WHERE Col IS NOT NULL
GROUP BY Col
HAVING COUNT(*) <> 10;
正如预期的那样,没有返回任何行。
我很高兴被证明是错误的,并承认我很幸运,因为这 100,000 次迭代中 none 发生了冲突,但我不认为这是运气。我真的相信只有一个锁,因此你是否有 t运行saction 并不重要,你只需要正确的隔离级别。
Serializable 隔离模式不一定锁定整个table。如果你在 BatchGuid 上有一个索引,你可能会做的很好,但如果没有,那么 SQL 可能会升级为 table 锁。
您可能想要查看的一些内容:
假设我在 SQL 服务器中有一个 table 作为需要处理的项目的队列。像这样:
Id (bigint)
BatchGuid (guid)
BatchProcessed (bit)
...
...以及其他一些描述需要处理的项目的列,等等。因此有许多 运行 消费者根据需要向此 table 添加记录以表明项目需要处理。
现在假设我有一份工作负责从这个 table 中获取一批项目并进行处理。假设我们想让它一次处理 10 个。现在还假设此作业可以同时拥有多个实例 运行,因此它正在同时访问 table(以及可能向队列中添加新记录的任何其他消费者)。
我正打算做这样的事情:
using(var tx = new Transaction(Isolation.Serializable))
{
var batchGuid = //newGuid
executeSql("update top(10) [QUeueTable] set [BatchGuid] = batchGuid where [BatchGuid] is null");
var itemsToProcess = executeSql("select * from [QueueTable] where [BatchGuid] = batchGuid");
tx.Commit()
}
所以基本上我要做的是启动一个可序列化的事务,用特定的 GUID 标记 10 个项目,然后获取这 10 个项目,然后提交。
这是可行的策略吗?我相信 serializable 的隔离级别基本上会锁定整个 table 以防止 read/write 直到事务完成 - 这是正确的吗?基本上,事务将阻止 table 上的所有其他 read/write 操作?我相信这就是我在这种情况下想要的,因为我不想读取脏数据并且我不希望并发 运行 作业在标记要处理的批次 10 时相互踩踏。
任何关于我是否走在正确轨道上的见解都将不胜感激。如果有更好的方法来实现这一点,我也欢迎其他选择。
如果使用 OUTPUT
子句,您可以在一条语句中执行此操作:
UPDATE TOP (10) [QueueTable]
OUTPUT inserted.*
SET [BatchGuid] = batchGuid
WHERE [BatchGuid] IS NULL;
或更具体地说:
var itemsToProcess = executeSql("update top(10) [QUeueTable] output inserted.* set [BatchGuid] = batchGuid where [BatchGuid] is null");
我想这是个人偏好,但我从来都不是 UPDATE TOP(n)
语法的粉丝,因为你不能指定 ORDER BY
,而且在大多数情况下指定 top 时,你想要指定一个顺序,我更喜欢使用类似的东西:
UPDATE q
OUTPTUT inserted.*
SET [BatchGuid] = batchGuid
FROM ( SELECT TOP (10) *
FROM dbo.QueueTable
WHERE BatchGuid IS NULL
ORDER BY ID
) AS q
附录
作为对评论的回应,我不认为存在竞争条件的任何可能性,但我不是 100% 确定。我之所以不相信这一点,是因为虽然查询读取为 SELECT,并且是 UPDATE,但它是语法糖,它只是一个更新,并且使用完全相同的计划,并且与顶级查询一样锁定。但是,由于我不确定我决定测试:
首先,我在临时数据库中设置了一个示例 table,并设置了一个日志记录 table 来记录更新的 ID
USE TempDB;
GO
CREATE TABLE dbo.T (ID BIGINT NOT NULL IDENTITY PRIMARY KEY, Col UNIQUEIDENTIFIER NULL);
INSERT dbo.T (Col)
SELECT TOP 1000000 NULL
FROM sys.all_objects a, sys.all_objects b;
CREATE TABLE dbo.T2 (ID BIGINT NOT NULL PRIMARY KEY);
然后在 10 个不同的 SSMS windows 我 运行 这个:
WHILE 1 = 1
BEGIN
DECLARE @ID UNIQUEIDENTIFIER = NEWID();
UPDATE T
SET Col = @ID
OUTPUT inserted.ID INTO dbo.T2 (ID)
FROM ( SELECT TOP 10 *
FROM dbo.T
WHERE Col IS NULL
ORDER BY ID
) t;
IF @@ROWCOUNT = 0
RETURN;
END
在我停止所有 10 个线程之前,整个过程 运行 更新了 ~500,000 行 20 分钟。由于在插入到 T2 时两次更新同一行会抛出错误并作为主键违规,并且需要停止所有 10 个线程,这表明不存在竞争条件,为了确认这一点,我 运行 以下内容:
SELECT Col, COUNT(*)
FROM dbo.T
WHERE Col IS NOT NULL
GROUP BY Col
HAVING COUNT(*) <> 10;
正如预期的那样,没有返回任何行。
我很高兴被证明是错误的,并承认我很幸运,因为这 100,000 次迭代中 none 发生了冲突,但我不认为这是运气。我真的相信只有一个锁,因此你是否有 t运行saction 并不重要,你只需要正确的隔离级别。
Serializable 隔离模式不一定锁定整个table。如果你在 BatchGuid 上有一个索引,你可能会做的很好,但如果没有,那么 SQL 可能会升级为 table 锁。
您可能想要查看的一些内容: