使用 SELECT-UPDATE 模式时管理并发

Eri*_*ikE 26 sql-server concurrency locking isolation-level

假设您有以下代码(请忽略它很糟糕):

BEGIN TRAN;
DECLARE @id int
SELECT @id = id + 1 FROM TableA;
UPDATE TableA SET id = @id; --TableA must have only one row, apparently!
COMMIT TRAN;
-- @id is returned to the client or used somewhere else
Run Code Online (Sandbox Code Playgroud)

在我看来,这不是正确管理并发性。仅仅因为您有一个事务并不意味着其他人不会读取您在获取更新语句之前所做的相同值。

现在,让代码保持原样(我意识到这作为单个语句更好地处理,甚至使用自动增量/标识列更好)有哪些确定的方法可以使其正确处理并发并防止允许两个客户端获得相同条件的竞争条件身份证价值?

我很确定将 a 添加WITH (UPDLOCK, HOLDLOCK)到 SELECT 会解决问题。该SERIALIZABLE事务隔离级别(因为它拒绝任何人阅读你做了什么,直到移植是在将似乎工作,以及UPDATE:这是假见马丁的答案)。真的吗?它们会同样有效吗?一个比另一个更受欢迎吗?

想象一下做一些比 ID 更新更合法的事情——一些基于你需要更新的读取的计算。可能涉及许多表,其中一些您会写入,而另一些则不会。这里的最佳做法是什么?

写完这个问题后,我认为锁定提示更好,因为这样你只锁定了你需要的表,但我很感激任何人的意见。

PS 不,我不知道最好的答案,真的很想得到更好的理解!:)

Mar*_*ith 12

只是针对 SERIALIZABLE隔离级别方面的问题。是的,这会起作用,但有死锁风险。

两个事务都可以同时读取该行。它们不会相互阻塞,因为它们将获取依赖于表结构的对象S锁或索引RangeS-S锁,并且这些锁是兼容的。但是在尝试获取更新所需的锁(分别为对象IX锁或索引RangeS-U)时,它们会相互阻塞,这将导致死锁。

UPDLOCK相反,使用显式提示将序列化读取,从而避免死锁风险。


A-K*_*A-K 11

我认为对您来说最好的方法是将您的模块实际暴露于高并发下并亲自查看。有时仅UPDLOCK就足够了,不需要HOLDLOCK。有时 sp_getapplock 效果很好。我不会在这里做任何一揽子声明——有时添加一个更多的索引、触发器或索引视图会改变结果。我们需要对代码进行压力测试,并根据具体情况亲自查看。

在这里写了几个压力测试的例子

编辑:为了更好地了解内部结构,您可以阅读 Kalen Delaney 的书。但是,书籍可能会像任何其他文档一样不同步。此外,还有太多组合需要考虑:六个隔离级别、多种锁、聚簇/非聚簇索引以及谁知道还有什么。那是很多组合。最重要的是,SQL Server 是封闭源代码,因此我们无法下载源代码、调试它等等——这将是知识的最终来源。在下一个版本或服务包发布后,其他任何内容都可能不完整或过时。

因此,如果没有自己的压力测试,您不应该决定什么对您的系统有效。无论你读过什么,它都可以帮助你理解正在发生的事情,但你必须证明你读到的建议对你有用。我想没有人能为你做这件事。


Mar*_*ith 9

在这种特殊情况下,向 中添加UPDLOCKSELECT确实可以防止异常。添加HOLDLOCK不是必需的,因为在事务期间持有更新锁,但我承认自己过去将它包含为(可能是坏的)习惯。

想象一下做一些比 ID 更新更合法的事情,一些基于你需要更新的读取的计算。可能涉及许多表,其中一些您会写入,而另一些则不会。这里的最佳做法是什么?

没有最佳实践。您选择的并发控制必须基于应用程序的要求。某些应用程序/事务需要像它们拥有数据库的专有所有权一样执行,不惜一切代价避免异常和不准确。其他应用程序/事务可以容忍彼此之间的某种程度的干扰。

  • 在网上商店中检索产品的带状库存水平(<5、10+、50+、100+)=脏读(不准确无关紧要)。
  • 检查和减少该网上商店结账时的库存水平 = 可重复读取(我们必须在销售前拥有库存,我们不得以负库存水平结束)。
  • 在银行的活期账户和储蓄账户之间移动现金 = 可序列化(不要计算错误或错放我的现金!)。

编辑:@AlexKuznetsov 的评论促使我重新阅读了问题并删除了答案中非常明显的错误。请注意深夜发帖时的自我。