对于 AI 代理:可在 https://www.mongodb.com/zh-cn/docs/llms.txt 获取文档索引—通过在任何 URL 路径后添加 .md 可获取所有页面的 Markdown 版本。
See how MongoDB 9.0 delivers up to 2x higher throughput.
MongoDB Branding Shape
Register now >
Docs 菜单

使用乐观锁防止更新冲突

在本指南中,您可以学习;了解如何使用MongoDB Extension for Hibernate ORM 来执行乐观锁。乐观锁允许并发会话读取同一实体,然后在多个会话尝试写入该实体时检测到冲突。

当您打开乐观锁时,Hibernate ORM 扩展会为每个更新或删除操作生成MongoDB查询语言 (MQL)声明。除了主键之外,该声明还筛选会话读取的版本值。如果另一个会话首先修改了该文档,则过滤无法匹配任何文档,并且 Hibernate ORM 会抛出 OptimisticLockException。

Hibernate ORM 扩展不支持悲观锁。悲观锁会在会话读取实体时锁定该实体,这会阻止其他会话修改该实体,直到该会话释放锁。

要为实体打开乐观锁,请在实体类中添加一个带有 @Version 注释的字段。

如果应用程序读取版本值,请为字段定义 getter,但不要定义 setter。 Hibernate ORM 自行分配该值,在应用程序代码中设置该字段可能会导致 Hibernate ORM 扩展生成与存储的文档不匹配的过滤。

本指南中的示例使用以下 Product 实体,该实体映射到 products集合。不要初始化 version字段,因为 Hibernate ORM 会在您首次持久化实体时为其分配初始值:

@Entity
@Table(name = "products")
public class Product {
@Id
@ObjectIdGenerator
private ObjectId id;
private String name;
private int quantity;
@Version
private Long version;
public Product() {
}
public Product(String name, int quantity) {
this.name = name;
this.quantity = quantity;
}
public ObjectId getId() {
return id;
}
public Long getVersion() {
return version;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

Hibernate ORM 扩展支持以下 @Version字段类型:

  • int 和 Integer

  • long 和 Long

  • Instant

数字版本字段从 0 开始,并在每次写入时按 1 递增。 Instant 版本字段存储写入时间。

当您持久保存新实体时,Hibernate ORM 会将版本字段设置为 0。当您修改实体并提交ACID 事务时,Hibernate ORM 扩展会生成一条更新声明,该语句会筛选会话读取的主键和版本值。该声明还设置新的版本值。

以下示例插入一个新的 Product 实体,然后更新其 quantity字段以递增版本:

var sf = HibernateUtil.getSessionFactory();
try (Session session = sf.openSession()) {
// Insert a new product, which sets the version to 0
session.beginTransaction();
var product = new Product("notebook", 100);
session.persist(product);
session.getTransaction().commit();
System.out.println("Version after insert: " + product.getVersion());
// Update the same product, which increments the version to 1
session.beginTransaction();
product.setQuantity(75);
session.getTransaction().commit();
System.out.println("Version after update: " + product.getVersion());
}
sf.close();

前面示例中的更新生成类似于以下MQL 的声明:

{
"update": "products",
"updates": [
{
"q": { "$and": [ { "_id": { "$eq": "..." } }, { "version": { "$eq": 0 } } ] },
"u": { "$set": { "quantity": 75, "version": 1 } }
}
]
}

删除操作使用相同的过滤。由于版本值是过滤的一部分,因此仅当文档仍包含会话读取的版本时,写入才会成功。

如果另一个会话在您的会话读取文档后对其进行了修改,则过滤中的版本值将不再与存储的值匹配,并且 Hibernate ORM 会抛出 OptimisticLockException。要恢复,请捕获异常并重新加载实体并重试写入,或者返回错误以通知应用程序的用户存在冲突。

以下示例使用两个会话来读取同一文档。第二个会话首先提交,因此第一个会话中的提交会抛出 OptimisticLockException:

var sf = HibernateUtil.getSessionFactory();
ObjectId productId;
// Insert a new product for two sessions to modify
try (Session session = sf.openSession()) {
session.beginTransaction();
var product = new Product("notebook", 100);
session.persist(product);
session.getTransaction().commit();
productId = product.getId();
}
try (Session sessionA = sf.openSession(); Session sessionB = sf.openSession()) {
// Both sessions load the product at version 0
var productA = sessionA.find(Product.class, productId);
var productB = sessionB.find(Product.class, productId);
// Session B commits first and increments the version to 1
sessionB.beginTransaction();
productB.setQuantity(75);
sessionB.getTransaction().commit();
// Session A commits second, but still holds version 0
try {
sessionA.beginTransaction();
productA.setQuantity(50);
sessionA.getTransaction().commit();
} catch (OptimisticLockException e) {
System.out.println("Update failed: Another session modified this document");
// Reload the entity and retry, or report the conflict to the user
}
}
sf.close();

注意

根据应用程序加载实体的方式,Hibernate ORM 可能会抛出 StaleObjectStateException。此异常在 Hibernate ORM原生相当于 OptimisticLockException。

要递增会话未修改的实体的版本,请将该实体和 LockMode.OPTIMISTIC_FORCE_INCREMENT锁模式传递给 Session 上的 lock() 方法。当对相关数据的更改必须使其他会话的实体副本失效时,请使用此锁模式。

以下示例递增 Product文档的版本:

var sf = HibernateUtil.getSessionFactory();
try (Session session = sf.openSession()) {
session.beginTransaction();
var product = session.createQuery("from Product where name = :n", Product.class)
.setParameter("n", "notebook")
.setMaxResults(1)
.getSingleResult();
session.lock(product, LockMode.OPTIMISTIC_FORCE_INCREMENT);
session.getTransaction().commit();
}
sf.close();

如果另一个会话首先递增了版本,则提交会抛出 OptimisticLockException。

您还可以在不添加版本字段的情况下检测冲突。为此,请使用 @OptimisticLocking 注释您的实体并指定以下类型之一:

  • OptimisticLockType.DIRTY:Hibernate ORM 扩展仅筛选会话修改的字段的先前值。

  • OptimisticLockType.ALL:Hibernate ORM 扩展会筛选所有实体字段的先前值。

这两种类型都需要 @DynamicUpdate 注解。

以下示例使用 DIRTY 类型:

@Entity
@Table(name = "products")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
public class Product {
@Id
@ObjectIdGenerator
private ObjectId id;
private String name;
private int quantity;
// Constructors, getters, and setters
}

要将字段从版本化实体中的乐观锁定中排除,请使用 @OptimisticLock(excluded = true) 注释该字段。对已排除字段的更改不会递增版本:

@Entity
@Table(name = "products")
public class Product {
@Id
@ObjectIdGenerator
private ObjectId id;
@OptimisticLock(excluded = true)
private String name;
private int quantity;
@Version
private Long version;
// Constructors, getters, and setters
}

乐观锁具有以下限制:

  • Hibernate ORM 扩展不支持悲观锁。要学习;了解有关锁定支持的更多信息,请参阅功能兼容性页面的事务和并发部分。

  • 不能将具有 @Version字段的实体传递给 StatelessSession.upsert() 方法。 Hibernate ORM 扩展会为此操作抛出一个 UnsupportedFeatureException。

要学习;了解有关乐观锁的更多信息,请参阅 Hibernate ORM 文档中的乐观锁。

要学习;了解有关在ACID 事务中运行写入操作的更多信息,请参阅事务和会话指南。