多表复杂实体的乐观并发
Posted
技术标签:
【中文标题】多表复杂实体的乐观并发【英文标题】:Optimistic concurrency on multi-table complex entity 【发布时间】:2014-03-02 11:20:45 【问题描述】:我有一个复杂的实体(我们称之为 Thing
),它在 SQL Server 中表示为多个表:一个父表 dbo.Thing
和多个子表 dbo.ThingBodyPart
、dbo.ThingThought
等。我们已经实现使用dbo.Thing
上的单个rowversion
列的乐观并发,使用UPDATE OUTPUT INTO
technique。这一直很好,直到我们向dbo.Thing
添加了一个触发器。我正在寻求有关选择不同方法的建议,因为我相当确信我目前的方法无法修复。
这是我们当前的代码:
CREATE PROCEDURE dbo.UpdateThing
@id uniqueidentifier,
-- ...
-- ... other parameters describing what to update...
-- ...
@rowVersion binary(8) OUTPUT
AS
BEGIN TRANSACTION;
BEGIN TRY
-- ...
-- ... update lots of Thing's child rows...
-- ...
DECLARE @t TABLE (
[RowVersion] binary(8) NOT NULL
);
UPDATE dbo.Thing
SET ModifiedUtc = sysutcdatetime()
OUTPUT INSERTED.[RowVersion] INTO @t
WHERE
Id = @id
AND [RowVersion] = @rowVersion;
IF @@ROWCOUNT = 0 RAISERROR('Thing has been updated by another user.', 16, 1);
COMMIT;
SELECT @rowVersion = [RowVersion] FROM @t;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK;
EXEC usp_Rethrow_Error;
END CATCH
在我们将INSTEAD OF UPDATE
触发器添加到dbo.Thing
之前,这非常有效。现在存储过程不再返回新的@rowVersion
值,而是返回未修改的旧值。我不知所措。是否有其他方法可以像上述方法一样有效和简单,但也可以与触发器一起使用?
为了说明这段代码究竟出了什么问题,请考虑以下测试代码:
DECLARE
@id uniqueidentifier = 'b0442c71-dbcb-4e0c-a178-1a01b9efaf0f',
@oldRowVersion binary(8),
@newRowVersion binary(8),
@expected binary(8);
SELECT @oldRowVersion = [RowVersion]
FROM dbo.Thing
WHERE Id = @id;
PRINT '@oldRowVersion = ' + convert(char(18), @oldRowVersion, 1);
DECLARE @t TABLE (
[RowVersion] binary(8) NOT NULL
);
UPDATE dbo.Thing
SET ModifiedUtc = sysutcdatetime()
OUTPUT INSERTED.[RowVersion] INTO @t
WHERE
Id = @id
AND [RowVersion] = @oldRowVersion;
PRINT '@@ROWCOUNT = ' + convert(varchar(10), @@ROWCOUNT);
SELECT @newRowVersion = [RowVersion] FROM @t;
PRINT '@newRowVersion = ' + convert(char(18), @newRowVersion, 1);
SELECT @expected = [RowVersion]
FROM dbo.Thing
WHERE Id = @id;
PRINT '@expected = ' + convert(char(18), @expected, 1);
IF @newRowVersion = @expected PRINT 'Pass!'
ELSE PRINT 'Fail. :('
当触发器不存在时,此代码正确输出:
@oldRowVersion = 0x0000000000016CDC
(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x000000000004E9D1
@expected = 0x000000000004E9D1
Pass!
当触发器存在时,我们没有收到预期值:
@oldRowVersion = 0x0000000000016CDC
(1 row(s) affected)
(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x0000000000016CDC
@expected = 0x000000000004E9D1
Fail. :(
对于不同的方法有什么想法吗?
我假设 UPDATE
是一个原子操作,它是,除非有触发器,但显然不是。我错了吗?在我看来,这似乎真的很糟糕,每个语句背后都潜藏着潜在的并发错误。如果触发器真的是INSTEAD OF
,我不应该取回正确的时间戳,就好像触发器的UPDATE
是我实际执行的那样吗?这是 SQL Server 错误吗?
【问题讨论】:
【参考方案1】:我的一位尊敬的同事Jonathan MacCollum 向我指出了这个bit of documentation:
插入
是一个列前缀,指定插入或更新操作添加的值。 以 INSERTED 为前缀的列反映了 UPDATE、INSERT 或 MERGE 语句完成之后但在触发器执行之前的值。
据此,我假设我需要修改我的存储过程,将UPDATE
拆分为UPDATE
,然后是SELECT [RowVersion] ...
。
UPDATE dbo.Thing
SET ModifiedUtc = sysutcdatetime()
WHERE
Id = @id
AND [RowVersion] = @rowVersion;
IF @@ROWCOUNT = 0 RAISERROR('Thing has been updated by another user.', 16, 1);
COMMIT;
SELECT @rowVersion = [RowVersion]
FROM dbo.Thing
WHERE Id = @id;
我认为我仍然可以放心,我的存储过程不会意外覆盖任何其他人的更改,但我不应再假设存储过程的调用者持有的数据仍然是最新的。存储过程返回的新@rowVersion
值有可能实际上是其他人更新的结果,而不是我的。所以实际上,根本没有返回@rowVersion
的意义。执行此存储过程后,调用者应重新获取Thing
及其所有子记录,以确保其数据图片一致。
... 这进一步让我得出结论,rowversion
列不是实现乐观锁定的最佳选择,遗憾的是,这是它们的唯一目的。使用手动递增的int
列会更好,查询如下:
UPDATE dbo.Thing
SET Version = @version + 1
WHERE
Id = @id
AND Version = @version;
Version
列在单个原子操作中被检查和递增,因此其他语句没有机会在其间溜走。我不必询问数据库是什么新值,因为我告诉它是什么新值。只要Version
列包含我期望的值(并且假设所有其他更新此行的人也在遵守规则 - 正确递增Version
),我可以知道Thing
仍然完全一样我离开它。至少,我认为……
【讨论】:
以上是关于多表复杂实体的乐观并发的主要内容,如果未能解决你的问题,请参考以下文章
您如何将乐观并发与 WebAPI OData 控制器一起使用
Elasticsearch 基于乐观锁的并发控制 --- 2022-04-03
ElasticSearch 并发冲突+悲观锁与乐观锁+基于_version和external version进行乐观锁并发控制