我正在尝试使用hibernate学习JPA,并使用MySQL作为数据库。
据我理解,
LockModeType.OPTIMISTIC:实体版本在当前正在运行的事务结束时被检查。 可重复读取:在同一事务中,所有一致的读取都读取该事务中第一个这样的读取所建立的快照。
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上的困惑。
发布于 2021-12-06 00:19:22
如果您的问题是,HBN实现(或JPA规范)中是否存在与以下语句相关的缺陷
如果事务T1在版本化的对象上调用类型为LockModeType.OPTIMISTIC的锁,则实体管理器必须确保以下两种现象都不会发生:
锁模式必须始终防止P1和P2.现象的发生
然后,答案是--是的,您是正确的:如果您在执行基于某些实体状态的计算,但您没有修改这些实体状态,则HBN只是在事务结束时发出select version from ... where id = ...,因此它不会看到其他事务由于RR隔离级别而发生的变化。然而,我不会说RC隔离级别在这个特定情况下表现得更好:从技术角度看,它的行为更正确,但从业务角度来看,完全不可靠,因为它取决于时间,所以不要依赖LockModeType.OPTIMISTIC --通过设计和使用其他技术(例如:
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来正确地控制行为。让我们考虑以下示例:
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);
}但是,这种实现有明显的缺陷:如果有人主动强暴用户帐户,并不是所有失败的auth尝试都会因为并发性而被记录下来(在这里,我没有注意它可能是可以接受的,因为我们迟早会锁定用户帐户)。如何解决这一问题?我们可以写这样的东西:
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,并且显然由于版本错配而失败。因此,为了使代码“正确”工作,我们有以下几个棘手的选项:
em.createQuery("select id from user where id=:id").setLockMode(LockModeType.PESSIMISTIC_WRITE).getFirstResult()类似(我相信这在RR模式下可能不起作用,而且在刷新调用之后会丢失数据)现在让我们假设我们需要在我们的用户实体中添加另一个业务数据,比如“这么信誉”,我们应该如何更新新的字段--记住有人可能会粗暴地强迫我们的用户?备选方案如下:
我确实相信这个UPD不会对您有多大帮助,但是它的目的是要证明在不了解目标模型的情况下讨论JPA领域的一致性是不值得的。
发布于 2021-12-03 23:43:11
要理解这一点,让我们快速查看hibernates乐观锁定是如何工作的:
SELECT ... WHERE id=xxx;),例如可以有一个version计数的1- 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使用repeatable_read隔离级别,第一个SELECT建立相同事务的后续SELECT读取的状态(快照)。但是,这里的关键是,UPDATE不对已建立的快照进行操作,而是对行的提交状态(在此期间可能已被其他提交的事务更改)进行操作。
因此,如果版本计数器已经被另一个提交的事务更新了,则更新实际上不会更新任何行,而hibernate可以检测到这一点。
另见:
https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html
https://stackoverflow.com/questions/70209372
复制相似问题