【问题标题】:Transaction isolation - INSERTS dependant on previous records values事务隔离 - INSERTS 依赖于以前的记录值
【发布时间】:2017-07-05 15:17:59
【问题描述】:

这个问题是相关的/来自对另一件事的讨论: What is the correct isolation level for Order header - Order lines transactions?

想象一下我们有通常的 Orders_Headers 和 Orders_LineItems 表的场景。还可以说,我们有一个特殊的业务规则说:

  1. 每个订单都有折扣字段,该字段是根据最后输入的订单所经过的时间计算的

  2. 如果过去 Y 小时内有超过 X 个订单,则会专门计算每个下一个订单折扣字段。

  3. 如果最近 10 个订单的平均频率高于每分钟 x 次,则专门计算每个下一个订单的折扣字段。

  4. 每个下一个订单的折扣字段都是专门计算的

这里的重点是表明每个订单都依赖于以前的订单,并且隔离级别至关重要。

我们有一个事务(只是显示的代码的逻辑):

BEGIN TRANSACTION

INSERT INTO Order_Headers...

SET @Id = SCOPE_IDENTITY()

INSERT INTO Order_LineItems...(using @Id)

DECLARE @SomeVar INT

--just example to show selecting previous x orders
--needed to calculate Discount value for new Order
SELECT @SomeVar = COUNT(*) Order_Headers
WHERE ArbitraryCriteria

UPDATE Order_Headers
SET Discount= UDF(@SomeVar)
WHERE Id = @Id

COMMIT

END TRANSACTION

我们还有另一个事务来读取订单:

SELECT TOP 10 * FROM Order_Headers
ORDER BY Id DESC

问题

  1. 第一个事务的 SNAPSHOT 隔离级别和第二个适当级别的 READ COMMITED 隔离级别吗?

  2. 是否有更好的方法来处理 CREATE/UPDATE 事务,或者是否可以这样做?

【问题讨论】:

  • 不要试图粗鲁...您的数据库是否设置为允许快照隔离?你知道这是什么吗?我不确定您对此了解多少,或者您是否刚刚在谷歌搜索答案时看到了该关键字。
  • 不粗鲁;)。是的,它已设置为允许快照隔离。我知道它是什么。不是专家,但我知道它背后的基本思想。这正是我问正确执行此操作的方法的原因,因为我从理论上理解每个隔离级别的含义,但没有像这样的示例的实际经验,也不知道不同方法的所有警告。希望这是有道理的?如果没有,请指出没有意义的地方,以便我澄清更多。
  • 在阅读了您上一个问题的 cmets 后,我建议您在该帖子中回复 @DanGuzman 并附上此问题的链接,让他有机会扩展他的 cmets。
  • 感谢您的建议。另外,如果您对此问题有想法/解决方案,我很想听听。

标签: sql sql-server transaction-isolation


【解决方案1】:

快照的问题不在于插入/读取(我假设您决定使用它)。它是关于更新的,你应该关心。

快照隔离级别使用行版本控制。这意味着每当您插入/更新/删除行时,这些行都会在 tempdb(版本存储,这些行的位置)中重复,并使用版本控制标签将其大小增加 14 个字节,以便您新启动的事务可以读取上次提交的事务的行。请记住,在您重建索引之前,这些调整大小的行将保持原样。

这应该是一个指标,如果您的表真的很忙,您的索引将被更快地进行碎片整理,并且会在您的临时服务器上增加一定数量的 % 开销。所以请记住这一点。

正如我所提到的,这里更令人担忧的是更新。

每当您插入/删除/更新行时,您都会在这些行上获得排他锁(稍后的对象),并且由于您的快照使用的是行版本控制,因此来自另一个事务的插入会在新行上添加排他锁,并且不是问题。但是,如果您尝试更新现有行并且会话 2 尝试在该行上获取 X 锁定,它将失败,因为会话 1 已经具有 X 锁定,这就是您将收到此消息的地方:

Read Committed 和 Serializable 已经很好地涵盖了这些问题,因此您可能希望采用这种方法并在实际实施之前测试所有解决方案。请记住,所有事务都会导致更新阻塞,并且快照/读取提交的快照将简单地失败。

我个人会使用读取提交的快照和更改的过程,在 catch 块中重新运行 N 次,但是嘿,这也有缺陷!

【讨论】:

  • 感谢您的回复。首先,只是说我还没有决定什么。在我看来,由于幻读,我无法绕过快照/序列化隔离级别。如果有的话,我很想听听其他方式。另外,我不确定我是否正确阅读了您的答案,但您似乎不喜欢快照的方向(我的印象在这里可能是错误的)而且我不清楚为什么。我没有提到的一件事是,在有问题的表上,SELECTS 将比 INSERT/UPDATES 多几个数量级。我不会认为它们是 INSERT 密集型的
  • 为了简短。如果您使用快照并同时运行其中 2 个事务,则稍后一个将失败并且您将收到错误消息。我推荐读提交快照,它与读提交不同,它没有幻读,它会导致死锁,你可以通过重新运行它来解决它,而不是报错。或者您可以简单地使用可序列化并防止任何这些发生(但并发性较低)
【解决方案2】:

serializable 选项:

通过updlockserializable 表提示使用悲观锁定策略来获取由where 条件指定的键范围锁(由支持索引支持以仅锁定查询所需的范围) :

declare @Id int, @SomeVar int;
begin tran;

  select @SomeVar = count(OrderDate) 
  from Order_Headers with (updlock,serializable) 
  where OrderDate >= '20170101';

  insert into Order_Headers (OrderDate, SomeVar)
    select sysdatetime(), @SomeVar;

  set @Id = scope_identity();

  insert into Order_LineItems (id,cols)
    select @Id, cols
    from @TableValuedParameter;

commit tran;

关于使用updlockserializable 表提示的原因和如何使用select 锁定键范围以及为什么需要两者的很好指南,请参见Sam Saffron''s upsert (update/insert) patterns

参考:

【讨论】:

  • 非常感谢 Sam Saffron 的 upsert 阅读!在相当短的文章中有这么多有用的信息。就死锁而言,UPDLOCK 似乎确实是更安全的解决方案。我认为这是我将使用的方向。也感谢您努力在您的答案中添加额外的参考资料!如果 Dan Guzman 出现在之前的 cmets 中,我也会稍等片刻,但到目前为止,你的答案是被接受的候选人。再次感谢!
  • @deezg 乐于助人!
  • @deezg,使用UPDLOCK,SERIALIZABLEUPDLOCK,HOLDLOCK 提示重构您的代码,就像@SqlZim 沿语句级可序列化一样,将提供您描述的行为。请注意,此方法将有效地进行单线程插入/更新,但降低并发性是避免死锁重试的代价。
猜你喜欢
  • 2011-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-04
  • 1970-01-01
  • 2018-09-04
  • 2017-03-07
相关资源
最近更新 更多