玩转JPA的并发和锁定 | Java提升营

玩转JPA的并发和锁定

假设您有多个用户使用的系统,其中每个用户都试图同时修改同一实体。当并发访问时,如何确保数据的完整性?

持久性提供了一种锁的策略来管理并发。锁有两种类型:乐观锁和悲观锁。在深入研究锁策略之前,让我们学习一下ACID事务。

ACID(原子性,一致性,隔离性,持久性)事务可确保数据库事务及时完成。关系数据库(例如MySQL,Postgres,SQL Server和Oracle)都符合ACID。

数据库事务可以分解为多个组件。仅当事务的所有组件都成功时,符合ACID的数据库才能确保事务得以持久和落实。如果任何一个组件发生故障,则事务将回滚,并且不会进行任何更改。

如果有多个事务,则需要定义锁策略,以确保数据的完整性。

让我们看看本文中的每种锁定策略。

乐观锁

JPA支持javax.persistence.Version注解,将属性标记为实体的版本属性。此属性可以是数字(int,long,short)或java.sql.Timestamp字段。每个实体只能存在一个带有@Version注解的属性。

此外,对于映射到多个表的实体,@Version属性必须在主表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class User {

@Id
@Column (name = "ID", nullable = false)
@GeneratedValue (strategy = GenerationType.AUTO)
private long id;

@Version
private long version;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
...
}

在这里,每个读取数据的事务都拥有version属性的值。在任何修改数据的事务之前,它将再次检查version属性的值。

如果此时值发生更改,则将引发OptimisticLockException并回滚事务。否则,事务将提交更新并增加version属性的值。

乐观锁模式

在乐观锁的情况下,JPA提供了两种锁模式:OPTIMISTICOPTIMISTIC_FORCE_INCREMENT。OPTIMISTIC获得有@Version属性的实体的读取锁。OPTIMISTIC_FORCE_INCREMENT为有@Version属性的实体获取读取锁,并增加该属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class UserService {
@PersistenceContext
private EntityManager entityManager;

public void updateUser(final String id) {
User user = entityManager.find(User.class, id);
entityManager.lock(user, LockModeType.OPTIMISTIC);
..
}
}

public void updateUser(final String id) {
User user = entityManager.find(User.class, id);
entityManager.lock(user, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
..
}

悲观锁

在悲观锁的情况下,JPA创建一个事务,该事务获得数据的锁,直到事务完成为止。这样可以防止在释放锁之前其他事务对实体进行任何更新。

当数据经常被多个事务访问和修改时,悲观锁定非常有用。

请记住,如果实体不易于频繁修改,则使用悲观锁定可能会导致应用性能下降。

悲观锁模式

JPA提供了三种悲观锁模式:PESSIMISTIC_READPESSIMISTIC_FORCE_INCREMENTPESSIMISTIC_WRITE

PESSIMISTIC_READ 获得数据的共享锁,防止更新或删除数据。其他事务可能会在锁定期间读取数据,但将无法修改或删除数据。

PESSIMISTIC_WRITE 获得数据的排他锁,防止读取、更新或删除数据。

PESSIMISTIC_FORCE_INCREMENT 和PESSIMISTIC_WRITE一样,另外增加@Version属性的值。

1
2
3
4
5
User user = entityManager.find(User.class,id);
entityManager.lock(user, LockModeType.PESSIMISTIC_WRITE);
user.getOrders().forEach(order -> { orderRepository.delete(order); });
...

如果无法获得悲观锁,则将导致事务回滚,并且将引发PessimisticLockException。但是,如果锁定数据失败不会导致事务回滚,则将抛出LockTimeoutException

结论

根据您的应用需求,选择正确的锁定策略并仅在必要时应用锁定策略。

给老奴加个鸡腿吧 🍨.