JPA中的悲观锁定

2023/05/18

1. 概述

当我们想要从数据库中检索数据时,有很多情况。有时我们想为自己锁定它以进行进一步处理,这样其他人就无法打断我们的操作。

我们可以想到两种允许我们这样做的并发控制机制:设置适当的事务隔离级别或对我们当前需要的数据设置锁定

事务隔离是为数据库连接定义的。我们可以配置它来保留不同程度的锁定数据。

但是,一旦连接被创建,隔离级别就会被设置,它会影响该连接中的每条语句。幸运的是,我们可以使用悲观锁定,它使用数据库机制来保留对数据的更细粒度的独占访问。

我们可以使用悲观锁来确保没有其他事务可以修改或删除保留数据。

我们可以保留两种类型的锁:独占锁和共享锁。当其他人持有共享锁时,我们可以读取但不能写入数据。为了修改或删除保留数据,我们需要有排它锁。

我们可以使用“SELECT … FOR UPDATE”语句获取独占锁。

2. 锁定模式

JPA规范定义了我们将要讨论的三种悲观锁模式:

  • PESSIMISTIC_READ允许我们获取共享锁并防止数据被更新或删除
  • PESSIMISTIC_WRITE允许我们获取独占锁并防止数据被读取、更新或删除
  • PESSIMISTIC_FORCE_INCREMENT的工作方式与PESSIMISTIC_WRITE类似,它还会递增版本化实体的版本属性

它们都是LockModeType类的静态成员,允许事务获取数据库锁。它们都被保留,直到事务提交或回滚。

值得注意的是,我们一次只能获得一把锁。如果不可能,则抛出PersistenceException

2.1 PESSIMISTIC_READ

每当我们只想读取数据而不遇到脏读时,我们可以使用PESSIMISTIC_READ(共享锁)。不过,我们将无法进行任何更新或删除

有时我们使用的数据库不支持PESSIMISTIC_READ锁,因此我们改为可以获取PESSIMISTIC_WRITE锁。

2.2 PESSIMISTIC_WRITE

任何需要获取数据锁并对其进行更改的事务都应该获取PESSIMISTIC_WRITE锁。根据JPA规范,持有PESSIMISTIC_WRITE锁将阻止其他事务读取、更新或删除数据。

请注意,某些数据库系统实现了多版本并发控制,允许读访问获取已经被阻塞的数据

2.3 PESSIMISTIC_FORCE_INCREMENT

此锁的工作方式与PESSIMISTIC_WRITE类似,但引入它是为了与版本化实体协作-具有用@Version标注的属性的实体。

版本化实体的任何更新都可以在获得PESSIMISTIC_FORCE_INCREMENT锁之前进行。获取该锁会导致更新版本列

由持久层提供者决定它是否支持未版本化实体的PESSIMISTIC_FORCE_INCREMENT。如果没有,它会抛出PersistenceException。

2.4 异常

最好知道在使用悲观锁定时可能会发生哪种异常。JPA规范提供了不同类型的异常:

  • PessimisticLockException表示获取锁或将共享锁转换为独占锁失败并导致语句级回滚。
  • LockTimeoutException表示获取锁或将共享锁转换为独占锁超时并导致语句级回滚。
  • PersistenceException表示发生持久性问题。PersistenceException及其子类型(除了NoResultException、NonUniqueResultException、LockTimeoutException和QueryTimeoutException),标记要回滚的活动事务

3. 使用悲观锁

有几种可能的方法可以在单个记录或一组记录上配置悲观锁。让我们看看如何在JPA中执行此操作。

3.1 查找

查找可能是最直接的方法。

将LockModeType对象作为参数传递给find方法就足够了:

entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);

3.2 查询

此外,我们还可以使用Query对象,并以锁定模式作为参数调用setLockMode setter:

Query query = entityManager.createQuery("from Student where studentId = :studentId");
query.setParameter("studentId", studentId);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
query.getResultList()

3.3 显式锁定

也可以手动锁定find方法检索到的结果:

Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.lock(resultStudent, LockModeType.PESSIMISTIC_WRITE);

3.4 刷新

如果我们想通过refresh方法覆盖实体的状态,我们也可以设置一个锁:

Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.refresh(resultStudent, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

3.5 命名查询

@NamedQuery注解也允许我们设置锁定模式:

@NamedQuery(name="lockStudent",
    query="SELECT s FROM Student s WHERE s.id LIKE :studentId",
    lockMode = PESSIMISTIC_READ)

4. 锁定范围

锁定范围参数定义了如何处理被锁定实体的锁定关系。可以仅在查询中定义的单个实体上获得锁定,或者另外阻止其关系。

要配置范围,我们可以使用PessimisticLockScope枚举。它包含两个值:NORMAL和EXTENDED。

我们可以通过将带有PessimisticLockScope值的参数’javax.persistence.lock.scope’传递给EntityManager、Query、TypedQuery或NamedQuery的适当方法来设置范围:

Map<String, Object> properties = new HashMap<>();
map.put("javax.persistence.lock.scope", PessimisticLockScope.EXTENDED);
    
entityManager.find(Student.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties);

4.1 PessimisticLockScope.NORMAL

PessimisticLockScope.NORMAL是默认范围。使用这个锁定范围,我们就锁定了实体本身。当与join继承一起使用时,它还会锁定祖先。

让我们看一下包含两个实体的示例代码:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {

    @Id
    private Long id;
    private String name;
    private String lastName;

    // getters and setters
}

@Entity
public class Employee extends Person {

    private BigDecimal salary;

    // getters and setters
}

当我们想要获得对Employee的锁定时,我们可以观察跨越这两个实体的SQL查询:

SELECT t0.ID, t0.DTYPE, t0.LASTNAME, t0.NAME, t1.ID, t1.SALARY
FROM PERSON t0, EMPLOYEE t1
WHERE ((t0.ID = ?) AND ((t1.ID = t0.ID) AND (t0.DTYPE = ?))) FOR UPDATE

4.2 PessimisticLockScope.EXTENDED

EXTENDED范围涵盖与NORMAL相同的功能。此外,它还能够阻止连接表中的相关实体

简而言之,它适用于使用@ElementCollection或@OneToOne、@OneToMany等注解的实体以及@JoinTable。

让我们看一下带有@ElementCollection注解的示例代码:

@Entity
public class Customer {

    @Id
    private Long customerId;
    private String name;
    private String lastName;
    @ElementCollection
    @CollectionTable(name = "customer_address")
    private List<Address> addressList;

    // getters and setters
}

@Embeddable
public class Address {

    private String country;
    private String city;

    // getters and setters
}

让我们分析一下搜索客户实体时的一些查询:

SELECT CUSTOMERID, LASTNAME, NAME 
FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE

SELECT CITY, COUNTRY, Customer_CUSTOMERID 
FROM customer_address 
WHERE (Customer_CUSTOMERID = ?) FOR UPDATE

我们可以看到有两个FOR UPDATE查询锁定了客户表中的一行以及连接表中的一行。

另一个需要注意的有趣事实是,并非所有持久性提供程序都支持锁定范围

5. 设置锁定超时

除了设置锁范围,我们还可以调整另一个锁参数-超时。超时值是我们希望在发生LockTimeoutException之前等待获取锁的毫秒数

我们可以通过使用具有适当毫秒数的属性“javax.persistence.lock.timeout”来更改超时值,类似于锁定范围。

也可以通过将超时值更改为0来指定“无等待”锁定。

但是,我们应该记住,有些数据库驱动程序不支持以这种方式设置超时值

Map<String, Object> properties = new HashMap<>(); 
map.put("javax.persistence.lock.timeout", 1000L); 

entityManager.find(Student.class, 1L, LockModeType.PESSIMISTIC_READ, properties);

6. 总结

当设置适当的隔离级别不足以应对并发事务时,JPA为我们提供了悲观锁定。它使我们能够隔离和编排不同的事务,这样它们就不会同时访问相同的资源。

为此,我们可以在所讨论的锁类型之间进行选择,然后修改诸如范围或超时之类的参数。

另一方面,我们应该记住,了解数据库锁与了解底层数据库系统的机制同样重要。

同样重要的是要记住,悲观锁的行为取决于我们使用的持久性提供程序(JPA实现者)。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章