首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >LockModeType.OPTIMISTIC和Mysql的默认隔离级别可重复读取不一起工作吗?

LockModeType.OPTIMISTIC和Mysql的默认隔离级别可重复读取不一起工作吗?
EN

Stack Overflow用户
提问于 2021-12-03 03:56:56
回答 2查看 262关注 0票数 2

我正在尝试使用hibernate学习JPA,并使用MySQL作为数据库。

据我理解,

LockModeType.OPTIMISTIC:实体版本在当前正在运行的事务结束时被检查。 可重复读取:在同一事务中,所有一致的读取都读取该事务中第一个这样的读取所建立的快照。

hibernate中的LockModeType.OPTIMISTIC不适用于MySQL的默认隔离级别,是真的吗?

假设我有以下代码:

代码语言:javascript
复制
tx.begin();
EntityManager em = JPA.createEntityManager();
Item item = em.find(Item.class, 1, LockModeType.OPTIMISTIC);
// Assume the item here has version = 0
// Read the item fields etc, during that another transaction commits and made item version increased to version = 1
tx.commit(); // Here Hibernate should execute SELECT during flushing to check version,
// i.e SELECT version FROM Item WHERE id = 1 
em.close();

我所期望的是,在刷新过程中,Hibernate会抛出OptimisticLockException,因为项目的版本不再是0。但是,由于隔离级别的原因,Hibernate在同一事务中仍然会看到版本=0中的项,而不会触发OptimisitcLockExcpetion。

我试图搜索,但似乎以前没有人提出这样的问题,希望有人能帮助我澄清我在OptimisticLock上的困惑。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2021-12-06 00:19:22

如果您的问题是,HBN实现(或JPA规范)中是否存在与以下语句相关的缺陷

如果事务T1在版本化的对象上调用类型为LockModeType.OPTIMISTIC的锁,则实体管理器必须确保以下两种现象都不会发生:

  • P1 (脏读):事务T1修改行。然后,另一个事务T2读取该行并在T1提交或回滚之前获取修改后的值。事务T2最终成功提交;不管T1是提交还是回滚,以及它是在T2提交之前还是之后提交事务。
  • P2 (不可重复读取):事务T1读取一行。然后,另一个事务T2在T1提交之前修改或删除该行。两个事务最终都成功地提交了

锁模式必须始终防止P1和P2.现象的发生

然后,答案是--是的,您是正确的:如果您在执行基于某些实体状态的计算,但您没有修改这些实体状态,则HBN只是在事务结束时发出select version from ... where id = ...,因此它不会看到其他事务由于RR隔离级别而发生的变化。然而,我不会说RC隔离级别在这个特定情况下表现得更好:从技术角度看,它的行为更正确,但从业务角度来看,完全不可靠,因为它取决于时间,所以不要依赖LockModeType.OPTIMISTIC --通过设计和使用其他技术(例如:

  • 将来自不同域的数据存储在不同的实体中
  • 利用@OptimisticLock注释防止在不需要版本时增加版本(实际上,这将通过HBN注释毒害您的域模型)
  • 将某些属性标记为updatable=false,并通过JPQL更新更新它们,以防止版本增量

UPD。

以P2为例,如果我真的需要T1 (仅读行)失败,如果T2 (修改/删除行)首先提交,那么我能想到的唯一解决办法就是使用LockModeType.OPTIMISTIC_FORCE_INCREMENT。因此,当T1提交时,它将尝试更新版本并失败。如果我们继续使用RR隔离级别,您能更详细地说明您在最后提供的3点如何帮助解决这种情况吗?

短篇小说:

LockModeType.OPTIMISTIC_FORCE_INCREMENT似乎不是一个很好的解决方案,因为它将reader转换为writer,因此增量版本将使writers和其他readers都失败。但是,在您的例子中,可能可以接受发出LockModeType.PESSIMISTIC_READ,对于某些DBs来说,它会转换为select ... from ... for share/lock in share mode,后者反过来只会阻塞writer和块(或失败) current reader,因此您将避免我们正在讨论的现象。

长话短说:

当我们开始考虑某些“业务一致性”时,JPA规范不再是我们的朋友,问题是他们用“拒绝现象”和“某人一定失败”来定义一致性,但没有从业务角度给我们提供任何线索和API来正确地控制行为。让我们考虑以下示例:

代码语言:javascript
复制
class User {
  @Id
  long id;
  @Version
  long version;
  boolean locked;
  int failedAuthAttempts;
}

我们的目标是在failedAuthAttempts超过某个阈值时锁定用户帐户。我们的问题的纯SQL解决方案非常简单明了:

代码语言:javascript
复制
update user
  set failed_auth_attempts = failed_auth_attempts + 1,
  locked = case failed_auth_attempts + 1 >= :threshold_value then 1 else 0 end
where id = :user_id

但JPA让一切变得复杂..。乍一看,我们天真的实现应该如下所示:

代码语言:javascript
复制
void onAuthFailure(long userId) {
  User user = em.find(User.class, userId);
  int failedAuthAttempts = user.failedAuthAttempts + 1;
  user.failedAuthAttempts = failedAuthAttempts;
  if (failedAuthAttempts >= thresholdValue) {
    user.locked = true;
  }
  em.save(user);
}

但是,这种实现有明显的缺陷:如果有人主动强暴用户帐户,并不是所有失败的auth尝试都会因为并发性而被记录下来(在这里,我没有注意它可能是可以接受的,因为我们迟早会锁定用户帐户)。如何解决这一问题?我们可以写这样的东西:

代码语言:javascript
复制
void onAuthFailure(long userId) {
  User user = em.find(User.class, userId, LockModeType.PESSIMISTIC_WRITE);
  int failedAuthAttempts = user.failedAuthAttempts + 1;
  user.failedAuthAttempts = failedAuthAttempts;
  if (failedAuthAttempts >= thresholdValue) {
    user.locked = true;
  }
  em.save(user);
}

?其实没有。问题在于,对于不存在于持久性上下文(即“未知实体”)中的实体,hibernate会发出select ... from ... where id=:id for update,但是对于已知的实体,它会发出select ... from ... where id=:id and version=:version for update,并且显然由于版本错配而失败。因此,为了使代码“正确”工作,我们有以下几个棘手的选项:

  • 产生另一笔交易(我相信在大多数情况下,这不是一个好的选择)
  • 通过选择查询锁定实体,即smth。与em.createQuery("select id from user where id=:id").setLockMode(LockModeType.PESSIMISTIC_WRITE).getFirstResult()类似(我相信这在RR模式下可能不起作用,而且在刷新调用之后会丢失数据)
  • 将属性标记为不可更新,并通过JPQL更新(纯SQL解决方案)更新它们。

现在让我们假设我们需要在我们的用户实体中添加另一个业务数据,比如“这么信誉”,我们应该如何更新新的字段--记住有人可能会粗暴地强迫我们的用户?备选方案如下:

  • 继续编写“棘手的代码”(实际上,这可能导致我们在更新实体之前总是需要锁定实体这一违反直觉的想法)。
  • 跨不同实体从不同域分离数据(听起来也有违直觉)
  • 使用混合技术

我确实相信这个UPD不会对您有多大帮助,但是它的目的是要证明在不了解目标模型的情况下讨论JPA领域的一致性是不值得的。

票数 2
EN

Stack Overflow用户

发布于 2021-12-03 23:43:11

要理解这一点,让我们快速查看hibernates乐观锁定是如何工作的:

  • 1:开始一个新的事务
  • 2:通过ID查找一个实体(hibernate发出一个SELECT ... WHERE id=xxx;),例如可以有一个version计数的1
  • 3:修改实体
  • 4:刷新对数据库的更改(例如,在提交事务之前自动触发):
代码语言:javascript
复制
- 4.1: hibernate issues an `UPDATE ... SET ..., version=2 WHERE id=xxx AND version=1` which returns the number of updated rows
- 4.2: hibernate checks whether there was one row actually updated, throwing a StaleStateException if not
  • 5:在异常情况下提交事务/回滚

使用repeatable_read隔离级别,第一个SELECT建立相同事务的后续SELECT读取的状态(快照)。但是,这里的关键是,UPDATE不对已建立的快照进行操作,而是对行的提交状态(在此期间可能已被其他提交的事务更改)进行操作。

因此,如果版本计数器已经被另一个提交的事务更新了,则更新实际上不会更新任何行,而hibernate可以检测到这一点。

另见:

https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

可重复读取隔离级别选择vs UPDATE...WHERE

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/70209372

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档