在Vlad Mihalcea的伟大博客的Preventing lost updates in long conversations中说
为防止丢失更新,我们必须具有应用程序级别的可重复性 读取以及并发控制机制。
我为什么需要"应用程序级别"可重复阅读?使用并发控制机制还不够吗?
注意:我已经用Q& A风格写了这个,因为阅读帖子让我怀疑使用Hibernate +乐观锁定的无状态后端的可能性。我已经做出了自己的结论(这解释了我自己的问题),但我仍然可以犯错误或遗漏。
答案 0 :(得分:0)
恕我直言存在三种丢失的更新:
第一个必须在数据库级别处理,如果我们不超过 我们的工具带中的隔离级别我们绝对不能使用READ_COMMITED。我们应该至少使用REPETEABLE_READ来保证数据的一致性。
如果我们的RDBMS使用两阶段锁定(如SQL Server),并发将受到影响,因为在读取查询中获得的共享锁将在事务的结束(提交/回滚)时释放。
相反,如果我们的RDBMS使用MVCC(如PostgreSQL),则可重复读取事务中的每个查询都会在事务中看到第一个非事务控制语句的开始时的快照,并且只会阻止写操作已经采用了独占锁,这意味着另一个事务正在写入相同的数据,如果此事务成功,则等待的事务必须回滚(错误:由于并发更新而无法序列化访问)。
Hibernate的乐观锁定使用与PostgreSQL中的REPETEABLE_READ相同的方法,并且都正确地避免了数据库级别的丢失更新。 主要区别在于REPETEABLE_READ无法考虑用户的思考时间,因为它不能进一步超越pyshical事务的界限,典型的Web应用程序需要在多个请求期间发生的读 - 修改 - 写对话模式。
在此对话中,有人可能会在2到3之间修改Alice在1处请求的产品,并且Alice不会看到将在3中使用Alice请求覆盖的修改。
这就像“无状态对话反模式”一样。好吧,我不认为那样,因为这是类型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
Alice仍然可以根据旧数据进行购买。
问题在于,在这种情况下,我们不考虑用户思考时间,这样就可以丢失类型3的更新。 这里的解决方案是使用像Vlad这样的N_VERSION将乐观锁定推送到应用程序层,但是他没有考虑将N_VERSION发送到客户端并且使用该选项我们可以拥有无状态后端。 甚至是Hibernate consider this option。 场景将是:
可以说N_VERSION可能在Javascript客户端中很容易被破坏(即使我有asked myself)但我们将有验证规则来维护业务不变量。
所以我不同意弗拉德。我认为我们只需要业务规则和并发控制机制:乐观或悲观锁定(虽然使用悲观锁定我们无法避免丢失类型3的更新)。
我认为应用程序级可重复读取的好处是使用扩展会话来维护请求之间的对象,从而避免在每个请求中重新获取它们。
答案 1 :(得分:0)
在logical transaction中,您可以拥有N个物理交易和N - 1个用户思考时间。
用户思考时间也是逻辑交易的一部分,所以基本上你只有1和2。
我没有看到如何在没有lost update或乐观锁定的用户思考时阻止pessimistic locking域模型验证。
使用悲观锁定,您可以防止丢失更新,但仅限于最后一次物理事务,但仅当您考虑在最后一个事务中加载的状态而不是在开头加载的状态时。这打破了应用程序级可重复读取保证。
现在,为什么还需要应用程序级可重复读取?
答案与隔离级别可重复读取的情况相同。通过可重复读取,您可以知道从读取到写入的可序列化流程。如果没有此保证,您可以允许丢失更新异常。数据库事务也保留相同的语义。加载项目后,您可以阻止丢失更新(2PL)或检测它(MVCCC)。应用程序级可重复读取也会这样做,但是在多请求逻辑事务的上下文中,因此您需要保留您读取的相同行值,并且需要强制执行锁定机制。由于悲观锁定不会扩展 在多个请求中,乐观锁定是唯一可行的锁定机制。
因此,这种方法基本上是MVCC外推到多个请求。