如果您的问题实际上是与以下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 域中的一致性是不值得的。