为什么我需要 "Application-level" 可重复读取?

Why should I need "Application-level" repeatable-reads?

在 Vlad Mihalcea 的伟大博客 Preventing lost updates in long conversations 中说

To prevent lost updates we must have application-level repeatable reads along with a concurrency control mechanisms.

为什么我需要 "Application-level" 可重复读取?并发控制机制还不够吗?

注意:我以问答的方式写了这篇文章,因为阅读 post 让我怀疑使用 Hibernate + 乐观锁定的无状态后端的可能性。我已经做出了自己的结论(解释回答我自己的问题)但我仍然会犯错误或遗漏。

恕我直言存在三种丢失的更新:

  • 破坏一致性(必须 100% 避免这种情况)并发生在两个或多个物理事务以并发方式修改相同数据并存在竞争条件的情况下。
  • 破坏一致性(必须 100% 避免这种情况)并发生在两个逻辑事务修改相同数据的情况下。
  • 在用户思考期间发生的那些(为了更好的用户体验,应该避免这种情况)。

第一个必须在数据库级别处理,如果我们没有超过 我们绝对不能使用工具带中的隔离级别 READ_COMMITED。我们至少应该使用REPETEABLE_READ来保证数据的一致性。

如果我们的 RDBMS 使用两阶段锁定(如 SQL 服务器),并发性将受到影响,因为在读取查询中获得的共享锁将在事务结束时(commit/rollback)被释放.

相反,如果我们的 RDBMS 使用 MVCC(如 PostgreSQL),可重复读取事务中的每个查询都会看到事务中第一个非事务控制语句开始时的快照,并将如果已采用独占锁,则仅阻止写操作,这意味着另一个事务正在写入相同的数据,如果此事务成功,则等待的事务必须回滚(错误:由于并发更新而无法序列化访问)。

Hibernate 的乐观锁定使用与 PostgreREPETEABLE_READSQL 相同的方法,两者都正确地避免了数据库级别的更新丢失。 主要区别在于 REPETEABLE_READ 无法考虑用户思考时间,因为它无法超越物理事务的边界,典型的 Web 应用程序需要在多个期间发生的读-修改-写对话模式请求。

  1. Alice 请求显示某个产品
  2. 从数据库中获取产品并返回给浏览器
  3. Alice 请求修改产品
  4. 产品必须更新并保存到数据库

在此对话中,可能会发生在 2 和 3 之间有人修改 Alice 在 1 中请求的产品,而 Alice 不会看到将被 Alice 在 3 中的请求覆盖的修改。

这就像 "The stateless conversation anti-pattern" 一样呈现。好吧,我不这么认为,因为这是类型 2 的丢失更新,必须通过域的验证规则来避免(图中未考虑)。

假设一个产品的最大库存是10。 Alice 请求类似于

POST /purchase
productId=1&quantity=3

然后处理该请求的服务将执行如下操作:

product = repository.retrieve(purchase.productId)
if (product.quantity + purchase.quantity <= 10)
  product.quantity = product.quantity + purchase.quantity
  repository.save(product)
  return Http.OK
else
  return Http.4XX

仍然正确的是,爱丽丝可能是根据旧数据进行购买。

  1. Alice 请求显示某个产品
  2. 产品(数量=5)从数据库中获取并返回给浏览器
  3. Bob post 一次购买(purchase.quantity=2 所以 product.quantity 现在是 7)
  4. 爱丽丝看到 product.quantity=5,她想要那个数量=8,所以做了一个 POST (purchase.quantity=3)
  5. product.quantity=7 + purchase.quantity=3 <= 10 returns true 所以保存数量=10 的产品。这不会违反业务规则,但会让爱丽丝感到惊讶。

问题是,在这种情况下,我们没有考虑用户思考时间,这允许丢失类型 3 的更新。 这里的解决方案是像 Vlad 所说的那样使用 N_VERSION 将乐观锁定推入应用程序层,但他没有考虑将 N_VERSION 发送到客户端并且没有那个选项我们可以有一个无状态后端。 甚至休眠 consider this option。 场景将是:

  1. Alice 请求显示某个产品
  2. 从数据库中获取产品(数量=5,版本=1)并返回给浏览器
  3. Bob post 一次购买(purchase.quantity=2 所以 product.quantity 现在是 7,版本=2)
  4. Alice post 一次购买 (purchase.quantity=3)
  5. 应用程序版本检查会抛出并发错误("Hey Alice, the product has been updated by another user")

可以说 N_VERSION 在 Javascript 客户端中很容易被破坏(即使我有 ),但我们将有验证规则来维护业务不变性。

所以我不同意弗拉德。我认为我们只需要业务规则和并发控制机制:乐观或悲观锁定(尽管使用悲观锁定我们无法避免类型 3 的丢失更新)。

在我看来,应用程序级可重复读取的好处是使用扩展会话维护请求之间的对象,避免在每个请求中重新获取它们。

在一个 logical transaction 中,您可以有 N 个物理交易和 N - 1 个用户思考时间。

用户思考时间也是逻辑事务的一部分,所以基本上你只有 1. 和 2.

我看不出如何防止 lost update with domain model validations in the user think-time without pessimistic locking 或乐观锁定。

使用悲观锁定,您可以防止丢失更新,但仅限于最后一个物理事务,但前提是您考虑在最后一个事务中加载的状态,而不是在开始时加载的状态。这打破了应用程序级别的可重复读取保证。

现在,为什么您仍然需要应用程序级可重复读取?

答案与隔离级别可重复读案例中的答案相同。通过可重复读取,您知道从读取到写入有一个可序列化的流程。如果没有此保证,您可以允许丢失更新异常。数据库事务也保留相同的语义。加载项目后,您可以防止丢失更新 (2PL) 或检测它 (MVCCC)。应用程序级可重复读取也会这样做,但是在多请求逻辑事务的上下文中,因此您需要保留读取的相同行值,并且需要强制执行锁定机制。由于悲观锁定不会扩展 在多个请求中,乐观锁定是唯一需要考虑的可行锁定机制。

因此,这种方法基本上是 MVCC 外推到多个请求。