【问题标题】:Prevent lost updates with high transaction isolation levels: Is this a common misconception?使用高事务隔离级别防止丢失更新:这是一个常见的误解吗?
【发布时间】:2022-12-16 07:08:45
【问题描述】:

我注意到我的应用程序经常将值写入数据库,这取决于以前的读取操作。一个常见的例子是用户可以存钱的银行账户:

void deposit(amount) {
    balance = getAccountBalance()
    setAccountBalance(balance + amount)
}

如果此方法同时被两个线程/客户端/ATM 调用,我想避免竞争条件,这样帐户所有者会赔钱:

balance = getAccountBalance()       |
                                    | balance = getAccountBalance()
setAccountBalance(balance + amount) |
                                    | // balance2 = getAccountBalance() // theoretical
                                    | setAccountBalance(balance + amount)
                                    V

我经常读到可重复读或者可序列化可以解决这个问题。即使是 german Wikipedia article for Lost Updates 也说明了这一点。翻译成英文:

隔离级别 RR(Repeatable Read)经常被提及作为丢失更新问题的解决方案。

This SO answer建议可序列化对于 SELECT 之后的 INSERT 的类似问题。

据我了解这个想法 - 在右侧的过程试图设置账户余额时,(理论上的)读取操作不会再返回相同的余额。因此写操作是不允许的。是的——如果你阅读this popular SO answer,它实际上听起来非常合适:

在 REPEATABLE READ 下,第二个 SELECT 保证至少显示从第一个 SELECT 返回的未更改的行。在那一分钟内,并发事务可能会添加新行,但无法删除或更改现有行。

但后来我想知道什么“它们不能被删除或改变”实际上意味着。如果您仍然尝试删除/更改它会发生什么?你会得到一个错误吗?或者您的交易会等到第一个交易完成并最终执行更新吗?这一切都不同了。在第二种情况下,您仍然会赔钱。

如果你阅读下面的 cmets,它会变得更糟,因为还有其他方法可以满足可重复读条件。例如快照技术:可以在​​左侧事务写入其值之前拍摄快照,如果稍后在右侧事务中发生第二次读取,这允许提供原始值。例如,请参阅MySQL manual

同一事务内的一致性读取读取第一次读取建立的快照

我得出的结论是,限制事务隔离级别可能是消除竞争条件的错误工具。如果它解决了问题(对于特定的 DBMS),则不是由于定义可重复读.而是因为特定的实现来实现可重复读条件。例如锁的使用。

所以,对我来说它看起来像这样:解决这个问题你真正需要的是一个锁定机制。一些 DBMS 使用锁来实现的事实可重复读被剥削。

这个假设是否正确?还是我对事务隔离级别的理解有误?


您可能会生气,因为这一定是关于该主题的第一百万个问题。问题是:示例银行账户场景绝对关键。就在那儿,应该绝对清楚发生了什么,但在我看来,似乎有太多误导和矛盾的信息和误解。

【问题讨论】:

  • 对于特定的银行情况,这就是为什么他们通常记录(银行)交易.单独的交易记录为单独的行,余额本身可能实际上是一个计算值。
  • 银行账户只是一个例子。但无论如何,我想知道如果你有数百万次转移,你是否还想计算价值。另外,我并不是说更新问题无法解决。例如,您可以实施“乐观锁”。我只是想确认高隔离级别不是一个实际的解决方案,尽管通常以其他方式传播。

标签: sql transactions isolation-level transaction-isolation snapshot-isolation


【解决方案1】:

在 SQL Server 中,REPEATABLE READ 和 SERIALIZABLE 都将通过使一个事务因死锁而失败来防止丢失更新。在这些隔离级别中,每个会话将在初始 SELECT 期间获取并持有目标行上的共享 (S) 锁。然后每个会话都会尝试获取行上的排他 (X) 锁来更新它,从而导致死锁。

如果你想避免丢失更新而不是让一个会话等待另一个会话完成,你必须在初始选择之前或期间创建一个更排他的锁。对此的正常模式是向初始选择添加 UPDLOCK 提示以指示“选择更新”。对于“select for update”,没有理由提高事务隔离级别。

Oracle 和 PostgreSQL 也有您可以使用的“select for update”语法。

【讨论】:

  • 我的实际问题是依赖于 SELECT 的 INSERT。我想阻止或失败来自并发事务的 SELECT 尝试。我不知道 FOR UPDATE 如何处理非(尚未)现有的条目。 Afaik FOR UPDATE 将锁定行或页。但最佳情况下,我想禁止/阻止对同一键的并发 SELECT,但允许对其他键进行 SELECT(即使行尚不存在)。另外,您的意思是“死锁”还是等待锁?我们必须以任何方式避免死锁挂起线程。关于我的问题 - 那么我了解到您确认隔离级别不是实际的解决方案?
  • 添加 HOLDLOCK 以同时获取键范围锁,这将在空键范围上创建 U 锁以阻止第二个 SELECT。
  • Lost update transational 异常只有在锁模式是悲观的情况下才能避免。在乐观锁定模型中,没有避免丢失更新事务异常的灵丹妙药......
  • 当然有。客户端时间戳/行版本是最常见的。
【解决方案2】:

丢失更新是一种事务异常,只有在事务使用乐观锁定时才会发生。它永远不会发生在悲观锁定中。

  • 某些 RDBMS 仅提供乐观锁定,对于 Oracle 数据库和 PostGreSQL
  • 其他一些 RDBMS 仅提供悲观锁定,就是这种情况 IBM DB2
  • 最后,Microsoft SQL Server 能够交替使用 乐观或悲观锁定取决于用户的选择,默认行为是悲观的

所以问题必须面对你使用哪种 RDBMS 以及你有哪种类型的锁定......

一些更多的信息.​​..

只有在以独占模式锁定开始时,才能保证成功完成执行写入的事务,同时通过确保锁定模式是悲观而非乐观的来在事务期间保持锁定。尽管如此,这种技术并不能防止死锁......

数学家 Edsger Dijkstra 解决了最后一个问题(银行家算法),证明有必要在开始更新数据(INSERT、UPDATE、DELETE...)之前,设置所有必要的锁来保护所处理的数据,这相当于只能独家访问所有处理数据...... Dijkstra 因对计算机科学的贡献而获得图灵奖!

换句话说,只有一个用户访问数据库! ...

总结一下...

下表给出了事务异常以及使用隔离级别时应避免的情况:

【讨论】:

  • 实际上,我确实在 SQL Server 上遇到了类似的问题,它是由两个并发线程引起的意外插入。我可能需要再写一个问题。我的问题是隔离级别是否是一个解决方案。你的回答也听起来好像锁定是解决方案,而不是隔离本身。那么我宁愿尝试在第一次读取时锁定资源,而不是希望高隔离级别会创建我需要的锁。
  • 我对您的评论的回答是对我原始帖子的编辑...
  • 那么您能否确认隔离级别不是 SELECT...UPDATE 竞争条件问题的解决方案,而是隔离级别是通过使用锁定技术实现的事实?
  • 目前尚不清楚您所说的“乐观”是什么意思,或者这将如何适用于可重复读取或可序列化。
【解决方案3】:

这里的问题是您在询问 SQL 标准定义的隔离级别需要什么来解决不属于该定义的并发异常。

SQL 标准仅定义隔离级别(Read UncommitedRead CommitedRepeatable ReadSerializable)如何映射到Dirty ReadNon-Repeatable ReadPhantom Read异常。没有提到Lost-Update,所以这 - 正如您正确指出的那样 - 取决于特定 DBMS 如何实现隔离级别。

据说 REPEATABLE_READ 足以防止 PostgreSQL 上的更新丢失,需要 SERIALIZABLE 来防止它在 MySQL 和 Oracle 上发生。

【讨论】:

    猜你喜欢
    • 2012-01-02
    • 2022-09-27
    • 2013-08-27
    • 1970-01-01
    • 2013-01-09
    • 2023-03-04
    • 2011-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多