恕我直言存在三种丢失更新:
- 破坏一致性(必须 100% 避免)并发生在两个或多个物理事务以并发方式修改相同数据并存在竞争条件的情况下。
- 破坏一致性(必须 100% 避免)并发生在两个逻辑事务修改相同数据的情况下。
- 在用户思考期间发生的情况(应该避免这种情况,以获得更好的用户体验)。
第一个必须在数据库级别处理,如果我们没有超过
我们工具带中的隔离级别绝对不能使用 READ_COMMITED。我们至少应该使用 REPETEABLE_READ 来保证数据的一致性。
如果我们的 RDBMS 使用两阶段锁定(如 SQL Server),并发性将受到影响,因为在读取查询中获得的共享锁将在事务结束(提交/回滚)时释放。
相反,如果我们的 RDBMS 使用 MVCC(如 PostgreSQL),则可重复读取事务中的每个查询都会看到事务中第一个非事务控制语句开始时的快照,并且只会阻塞写入操作,如果排他锁已被占用,这意味着另一个事务正在写入相同的数据,如果此事务成功,则等待的必须回滚(错误:由于并发更新而无法序列化访问)。
Hibernate 的乐观锁定使用与 PostgreSQL 中的 REPETEABLE_READ 相同的方法,并且都正确地避免了数据库级别的丢失更新。
主要区别在于 REPETEABLE_READ 不能考虑用户的思考时间,因为它不能进一步超出 pyshical 事务的边界,并且典型的 Web 应用程序需要在多个请求期间发生的读取-修改-写入会话模式。
- Alice 要求展示某种产品
- 从数据库中获取产品并返回到浏览器
- Alice 要求修改产品
- 产品必须更新并保存到数据库中
在此对话中,可能有 2 到 3 个人修改了 Alice 在 1 处请求的产品,而 Alice 看不到该修改将被 Alice 3 中的请求覆盖。
这类似于“无状态对话反模式”。好吧,我不这么认为,因为这是类型 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 可能会根据旧数据进行购买。
- Alice 要求展示某种产品
- 从数据库中获取产品(数量=5)并返回到浏览器
- Bob 发布购买(purchase.quantity=2 所以 product.quantity 现在是 7)
- Alice 看到 product.quantity=5,她希望该数量=8,因此创建了一个 POST (purchase.quantity=3)
- product.quantity=7 + purchase.quantity=3
问题在于,在这种情况下,我们没有考虑用户思考时间,这会导致类型 3 的更新丢失。
这里的解决方案是使用 N_VERSION 将乐观锁定推送到应用程序层,就像 Vlad 说的那样,但他没有考虑将 N_VERSION 发送到客户端,并且没有这个选项,我们可以拥有一个无状态的后端。
甚至休眠consider this option。
场景是:
- Alice 要求展示某种产品
- 从数据库中获取产品(数量=5,版本=1)并返回到浏览器
- Bob 发布购买(purchase.quantity=2 所以 product.quantity 现在是 7 并且 version=2)
- Alice 发布购买 (purchase.quantity=3)
- 应用程序版本检查将引发并发错误(“Hey Alice,该产品已被其他用户更新”)
可以说 N_VERSION 在 Javascript 客户端中很容易被破坏(即使我有 asked myself),但我们将有验证规则来维护业务不变量。
所以我不同意弗拉德。我认为我们只需要业务规则和并发控制机制:乐观或悲观锁定(尽管使用悲观锁定我们无法避免丢失类型 3 的更新)。
在我看来,应用程序级可重复读取的好处是使用扩展会话来维护请求之间的对象,从而避免在每个请求中重新获取它们。