LockModeType.OPTIMISTIC 和 Mysql 的默认隔离级别 REPEATABLE READ 不能一起工作?
LockModeType.OPTIMISTIC and Mysql's default isolation level REPEATABLE READ don't work together?
我正在尝试使用 Hibernate 学习 JPA 并使用 MySQL 作为数据库。
据我了解,
LockModeType.OPTIMISTIC: The entity version is checked towards the end
of the currently running transaction.
REPEATABLE READ: All consistent reads within the same transaction read
the snapshot established by the first such read in that transaction
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的困惑。
为了理解这一点,让我们快速了解一下休眠乐观锁定的工作原理:
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
其中 returns 更新行数
- 4.2: hibernate 检查是否有一行实际更新,如果没有则抛出 StaleStateException
5:异常时提交事务/回滚
使用 repeatable_read 隔离级别,第一个 SELECT
建立状态(快照),同一事务的后续 SELECT
读取该状态(快照)。然而,这里的关键是 UPDATE
确实 而不是 对已建立的快照进行操作,而是对行的已提交状态(可能已被其他已提交的事务更改)同时)。
因此,如果在此期间版本计数器已被另一个提交的事务更新,则更新实际上不会更新任何行,而休眠可以检测到这一点。
另见:
https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html
如果您的问题实际上是 HBN 实现(或 JPA 规范)中是否存在与以下相关的缺陷 statement:
If transaction T1 calls for a lock of type LockModeType.OPTIMISTIC on
a versioned object, the entity manager must ensure that neither of the
following phenomena can occur:
- P1 (Dirty read): Transaction T1 modifies a row. Another transaction T2 then reads that row and obtains the modified value, before T1 has
committed or rolled back. Transaction T2 eventually commits
successfully; it does not matter whether T1 commits or rolls back and
whether it does so before or after T2 commits.
- P2 (Non-repeatable read): Transaction T1 reads a row. Another transaction T2 then modifies or deletes that row, before T1 has
committed. Both transactions eventually commit successfully.
Lock modes must always prevent the phenomena P1 and P2.
那么答案是是的,你是对的:如果你基于某些实体状态执行计算,但你没有修改那些实体状态,HBN 只是问题select version from ... where id = ...
在事务结束时,因此由于 RR 隔离级别,它看不到来自其他事务的更改。但是,我不会说 RC 隔离级别在这种特殊情况下表现得更好:从 技术角度来看它的行为更正确 但从 业务角度来看它是完全不可靠的 因为它取决于时间,所以不要依赖 LockModeType.OPTIMISTIC - 它在设计上是不可靠的并使用其他技术,如:
- 将来自不同域的数据存储在不同的实体中
- 利用 @OptimisticLock annotation 来防止在不需要时增加版本(实际上这会通过 HBN 注释毒害您的域模型)
- 将一些属性标记为
updatable=false
并通过JPQL更新来更新它们以防止版本增加
UPD.
Taking the P2 as example, if I really need T1 (only read row) to fail if T2 (modify/delete row) commits first, the only workaround I can think of is to use LockModeType.OPTIMISTIC_FORCE_INCREMENT. So when T1 commits it will try to update the version and fail. Can you elaborate more on how your provided 3 points at the end can help with this situation if we keep using RR isolation level?
短篇小说:
LockModeType.OPTIMISTIC_FORCE_INCREMENT
似乎不是一个好的解决方法,因为它将 reader
变成了 writer
,因此递增版本将同时失败 writers
和其他 [=20] =].但是,在您的情况下,发布 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;
}
我们的目标是在失败的 AuthAttempts 超过某个阈值时锁定用户帐户。我们问题的纯 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
并且由于版本不匹配而明显失败。所以我们有以下棘手的选项来使我们的代码“正确”工作:
- 产生另一个交易(我相信在大多数情况下这不是一个好的选择)
- 通过 select 查询锁定实体,即 smth。像
em.createQuery("select id from user where id=:id").setLockMode(LockModeType.PESSIMISTIC_WRITE).getFirstResult()
(我相信在 RR 模式下可能无法工作,而且在刷新调用后会丢失数据)
- 将属性标记为不可更新并通过 JPQL 更新(纯 SQL 解决方案)更新它们
现在假设我们需要将另一个业务数据添加到我们的用户实体中,比如“SO 声誉”,我们应该如何更新新字段,同时牢记有人可能会暴力破解我们的用户?选项如下:
- 继续编写“棘手的代码”(实际上这可能会导致我们产生违反直觉的想法,即我们总是需要在更新之前锁定实体)
- 跨不同实体拆分来自不同域的数据(听起来也违反直觉)
- 使用混合技术
我相信这个 UPD 不会对你有太大帮助,但它的目的是证明在不了解目标模型的情况下不值得讨论 JPA 域中的一致性。
我正在尝试使用 Hibernate 学习 JPA 并使用 MySQL 作为数据库。
据我了解,
LockModeType.OPTIMISTIC: The entity version is checked towards the end of the currently running transaction.
REPEATABLE READ: All consistent reads within the same transaction read the snapshot established by the first such read in that transaction
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的困惑。
为了理解这一点,让我们快速了解一下休眠乐观锁定的工作原理:
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
其中 returns 更新行数 - 4.2: hibernate 检查是否有一行实际更新,如果没有则抛出 StaleStateException
- 4.1: hibernate 发出一个
5:异常时提交事务/回滚
使用 repeatable_read 隔离级别,第一个 SELECT
建立状态(快照),同一事务的后续 SELECT
读取该状态(快照)。然而,这里的关键是 UPDATE
确实 而不是 对已建立的快照进行操作,而是对行的已提交状态(可能已被其他已提交的事务更改)同时)。
因此,如果在此期间版本计数器已被另一个提交的事务更新,则更新实际上不会更新任何行,而休眠可以检测到这一点。
另见:
https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html
如果您的问题实际上是 HBN 实现(或 JPA 规范)中是否存在与以下相关的缺陷 statement:
If transaction T1 calls for a lock of type LockModeType.OPTIMISTIC on a versioned object, the entity manager must ensure that neither of the following phenomena can occur:
- P1 (Dirty read): Transaction T1 modifies a row. Another transaction T2 then reads that row and obtains the modified value, before T1 has committed or rolled back. Transaction T2 eventually commits successfully; it does not matter whether T1 commits or rolls back and whether it does so before or after T2 commits.
- P2 (Non-repeatable read): Transaction T1 reads a row. Another transaction T2 then modifies or deletes that row, before T1 has committed. Both transactions eventually commit successfully.
Lock modes must always prevent the phenomena P1 and P2.
那么答案是是的,你是对的:如果你基于某些实体状态执行计算,但你没有修改那些实体状态,HBN 只是问题select version from ... where id = ...
在事务结束时,因此由于 RR 隔离级别,它看不到来自其他事务的更改。但是,我不会说 RC 隔离级别在这种特殊情况下表现得更好:从 技术角度来看它的行为更正确 但从 业务角度来看它是完全不可靠的 因为它取决于时间,所以不要依赖 LockModeType.OPTIMISTIC - 它在设计上是不可靠的并使用其他技术,如:
- 将来自不同域的数据存储在不同的实体中
- 利用 @OptimisticLock annotation 来防止在不需要时增加版本(实际上这会通过 HBN 注释毒害您的域模型)
- 将一些属性标记为
updatable=false
并通过JPQL更新来更新它们以防止版本增加
UPD.
Taking the P2 as example, if I really need T1 (only read row) to fail if T2 (modify/delete row) commits first, the only workaround I can think of is to use LockModeType.OPTIMISTIC_FORCE_INCREMENT. So when T1 commits it will try to update the version and fail. Can you elaborate more on how your provided 3 points at the end can help with this situation if we keep using RR isolation level?
短篇小说:
LockModeType.OPTIMISTIC_FORCE_INCREMENT
似乎不是一个好的解决方法,因为它将 reader
变成了 writer
,因此递增版本将同时失败 writers
和其他 [=20] =].但是,在您的情况下,发布 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;
}
我们的目标是在失败的 AuthAttempts 超过某个阈值时锁定用户帐户。我们问题的纯 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
并且由于版本不匹配而明显失败。所以我们有以下棘手的选项来使我们的代码“正确”工作:
- 产生另一个交易(我相信在大多数情况下这不是一个好的选择)
- 通过 select 查询锁定实体,即 smth。像
em.createQuery("select id from user where id=:id").setLockMode(LockModeType.PESSIMISTIC_WRITE).getFirstResult()
(我相信在 RR 模式下可能无法工作,而且在刷新调用后会丢失数据) - 将属性标记为不可更新并通过 JPQL 更新(纯 SQL 解决方案)更新它们
现在假设我们需要将另一个业务数据添加到我们的用户实体中,比如“SO 声誉”,我们应该如何更新新字段,同时牢记有人可能会暴力破解我们的用户?选项如下:
- 继续编写“棘手的代码”(实际上这可能会导致我们产生违反直觉的想法,即我们总是需要在更新之前锁定实体)
- 跨不同实体拆分来自不同域的数据(听起来也违反直觉)
- 使用混合技术
我相信这个 UPD 不会对你有太大帮助,但它的目的是证明在不了解目标模型的情况下不值得讨论 JPA 域中的一致性。