【问题标题】:LockModeType.OPTIMISTIC and Mysql's default isolation level REPEATABLE READ don't work together?LockModeType.OPTIMISTIC 和 Mysql 的默认隔离级别 REPEATABLE READ 不能一起工作?
【发布时间】:2022-01-09 12:43:25
【问题描述】:

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

据我了解

LockModeType.OPTIMISTIC:实体版本在最后检查 当前正在运行的事务。

REPEATABLE READ:同一事务中的所有一致读取读取 该事务中第一次此类读取所建立的快照

Hibernate 中的 LockModeType.OPTIMISTIC 是不是在 MySQL 的默认隔离级别下不起作用?

假设我有以下代码:

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 的困惑。

【问题讨论】:

  • Select 不会检查版本。只更新和删除
  • @SimonMartinelli LockModeType.OPTIMISTIC 是“读取”乐观锁,document 声明在当前运行的事务结束时检查实体版本。。即使实体在事务过程中没有改变,hibernate 是否仍然检查版本?否则,如果我需要“读取”乐观锁,乐观锁如何工作?

标签: java mysql hibernate jpa optimistic-locking


【解决方案1】:

如果您的问题实际上是与以下statement 相关的 HBN 实现(或 JPA 规范)存在缺陷:

如果事务 T1 调用 LockModeType.OPTIMISTIC 类型的锁 版本化的对象,实体管理器必须确保 可能会出现以下现象:

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

锁定模式必须始终防止出现 P1 和 P2 现象。

那么答案是是的,你是对的:如果你基于某个实体状态执行计算,但你没有修改这些实体状态,HBN 只会在事务结束,因此由于 RR 隔离级别,它看不到其他事务的更改。但是我不会说 RC 隔离级别在这种特殊情况下表现得更好:从 技术角度 它的行为更正确,但从 业务角度 完全不可靠,因为它取决于时间,所以不要依赖 LockModeType.OPTIMISTIC - 它在设计上是不可靠的,并使用其他技术,如:

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

更新。

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

短篇小说:

LockModeType.OPTIMISTIC_FORCE_INCREMENT 似乎不是一个好的解决方法,因为它会将reader 变成writer,因此递增版本将失败writers 和其他readers。但是,在您的情况下,发出LockModeType.PESSIMISTIC_READ 可能是可以接受的,对于某些数据库,它会转换为select ... from ... for share/lock in share mode,而这又会阻止writer 并阻止(或失败)current reader,因此您将避免我们正在谈论的现象关于。

长篇大论:

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

class User {
  @Id
  long id;
  @Version
  long version;
  boolean locked;
  int failedAuthAttempts;
}

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

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 让一切变得复杂......乍一看,我们的幼稚实现应该是这样的:

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);
}

但是该实现有明显的缺陷:如果有人主动暴力破解用户帐户,则并非所有失败的身份验证尝试都会由于并发而被记录(这里我没有注意它可能是可以接受的,因为迟早我们会锁定用户帐户)。如何解决此类问题?我们可以这样写:

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);
}

?其实没有。问题在于持久性上下文中不存在的实体(即“未知实体”)休眠问题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 解决方案)对其进行更新

现在假设我们需要将另一个业务数据添加到我们的用户实体中,比如“SO 信誉”,我们应该如何更新新字段,记住有人可能会暴力破解我们的用户?选项如下:

  • 继续编写“棘手的代码”(实际上这可能会导致我们产生违反直觉的想法,即我们总是需要在更新实体之前锁定它)
  • 将来自不同域的数据拆分到不同实体(听起来也违反直觉)
  • 使用混合技术

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

【讨论】:

  • 谢谢。答案的前半部分正是我想要的。以 P2 为例,如果我真的需要 T1(仅读取行)在 T2(修改/删除行)首先提交时失败,我能想到的唯一解决方法是使用 LockModeType.OPTIMISTIC_FORCE_INCREMENT。因此,当 T1 提交时,它将尝试更新版本并失败。如果我们继续使用 RR 隔离级别,您能否详细说明您最后提供的 3 点如何帮助解决这种情况?
  • @Curry 请检查更新
  • @Andrew B. Panfilov 您指出对已知实体的版本悲观写入会导致select ... from ... where id=:id and version=:version for update,这对我来说是新的(它看起来像是乐观锁定和悲观锁定的混合体) .我猜根据情况,可能需要对已知实体进行版本检查,因为已知实体不再是最新版本,应该抛出 OptimisticLockExcpetion。感谢您的更新!
  • 这看起来像“快速失败”:如果 HBN 知道实体,它会在刷新时发出“update ... where id=:id and version=:version”,并且由于版本不匹配而失败,所以,当锁定可能是合理的时候做同样的事情:我们已经知道事务会失败,为什么不现在就失败呢。另一方面,我确实相信“锁定和刷新”模式有权存在。
  • 对于“锁定和刷新”,我相信em.refresh(entity, LockModeType.PESSIMISTIC_WRITE); 可以完成工作。即使实体是已知实体,Hibernate 也会在没有任何版本的情况下发出 SELECT ... FROM ... WHERE id = ? for update,这样做会导致刷新之前对实体的所有更改都消失了。
【解决方案2】:

为了理解这一点,让我们快速了解一下 hibernate 乐观锁定的工作原理:

  • 1:开始一个新事务

  • 2:通过 ID 查找实体(hibernate 发出 SELECT ... WHERE id=xxx;),例如可以有 version 计数 1

  • 3:修改实体

  • 4:刷新对数据库的更改(例如,在提交事务之前自动触发):

    • 4.1:hibernate 发出一个UPDATE ... SET ..., version=2 WHERE id=xxx AND version=1,它返回更新的行数
    • 4.2:hibernate 检查是否有一行实际更新,如果没有则抛出 StaleStateException
  • 5:在异常情况下提交事务/回滚

使用repeatable_read 隔离级别,第一个SELECT 建立同一事务的后续SELECTs 读取的状态(快照)。然而,这里的关键是UPDATE 确实不是在已建立的快照上运行,而是在行的已提交状态(同时可能已被其他已提交事务更改)。

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

另见:
https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html
Repeatable Read isolation level SELECT vs UPDATE...WHERE

【讨论】:

  • 感谢您的回答。但是我面临的困惑是,当使用LockModeType.OPTIMISTIC 而不修改实体时,休眠将在事务结束时发出select version from ... where id = ...,即使另一个事务修改了实体,由于RR,它也总是返回相同的版本mysql中的隔离级别。
  • 我明白了,当然很抱歉我错过了您没有修改实体的要点
猜你喜欢
  • 1970-01-01
  • 2022-09-23
  • 2012-04-07
  • 2014-10-05
  • 2019-09-23
  • 2015-06-19
  • 2021-01-30
  • 2013-12-22
  • 1970-01-01
相关资源
最近更新 更多