在JPA和EJB中用乐观锁解决并发问题
同一条记录不同的用户都有权限修改,如:有一条记录编号为100,有一个字段price,张三修改price的值为200,李时修改其值为300。后修改的会覆盖前修改的,张三在修记录编号为100的记录过程中,中途去了躺厕所,回来继续修改。而李四在张三上厕所的过程中已经将数据修改为300,张三上厕所回来后修改为200,张三的修改覆盖了李四的修改,李四不知情。
这些便是并发产生的问题。
正常情况是:李三和李四同时修改同一条记录,后保存的不应该覆盖先保存的; 被删除的记录不能再被修改,而且要给用户提示。
数据库中的并发是指 DBMS 在不损害数据完整性或一致性的情况下管理来自不同用户或进程的许多并发活动或事务的能力。考虑到许多应用程序需要多个用户或进程对数据库进行并发访问,并发是现代数据库的重要组成部分。
尝试同时读取或写入数据库可能会导致出现多个问题,包括:
- 数据一致性 — 同时处理数据库中的数据的事务可能会导致数据的准确性和有效性出现问题。
- 死锁 — 由于 2 个或多个事务尝试更改和访问数据库中的相同资源,因此可能会发生死锁。这就像不同的进程相互阻止彼此获得所需的资源,从而导致无限等待。
- 性能 — 过多的锁定和事务序列化会在数据库响应时间中发挥重要作用,这是由对数据库中数据的并发访问触发的。
好消息是,数据库产品提供了开箱即用的解决方案,您可以使用锁定、隔离级别、时间戳或多版本并发控制 (MVCC) 来克服这些问题。
在本文中,我们将使用 JPA 版本注释,通过利用 Optimistic Lock 来帮助克服其中的一些问题。
乐观锁定与悲观锁定
根据您正在处理的数据的性质,您可以选择正确的方法来锁定 Database records。
- 乐观锁定 — 在这种方法中,多个用户可以尝试使用乐观锁定同时更改同一记录,而不会意识到其他用户的尝试,并且只有当其他用户尝试提交其并发更新时,他们才会收到存在冲突的警报。
- 悲观锁定 — 悲观锁定方法禁止并发记录更新。一旦一个人开始更新记录,就会对记录应用锁定,并且尝试更新此记录的用户会收到另一个用户当前正在进行更新的警报。通俗地说悲观锁锁定数据记录后别的用户无法再修改,直到当前用户修改完成。
使用 @Version 的乐观锁定
使用 @Version 非常简单。您只需在 Entity 类中有一个具有注释的字段,它是以下类型之一:int、Integer、long、Long、short、Short。
@Entity
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
@Version
private Integer version;
}
数据库中必须有对应的字段,否则不行。
JPA 提供了两种不同的乐观锁模式:
- OPTIMISTIC – 对于具有 version 属性的所有实体,您将获得乐观读取锁。
- OPTIMISTIC_FORCE_INCREMENT — 就像 OPTIMISTIC 一样,但 version 属性值增加了一个增量。
以下是一些如何使用它的示例。
entityManager.find(Product.class, id, LockModeType.OPTIMISTIC);
或者使用注释来加锁。
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query("SELECT p FROM Product p WHERE p.Id = ?1")
public Optional<Product> getProductById(Long id);
使用@version和OPTIMISTIC 后,做并发操作会报OptimisticLockException异常:
cannot be merged because it has changed or been deleted since it was last read.
这个异常OptimisticLockException在Backing Bean中捕获不到,必须自定义异常类。
自定义并发异常类
已删除的异常
import jakarta.ejb.EJBException;
public class DeletedException extends EJBException {
private static final long serialVersionUID = -3077279713283364443L;
public DeletedException() {
super();
}
public DeletedException(String message) {
super(message);
}
public DeletedException(Exception ex) {
super(ex);
}
public DeletedException(String message, Exception ex) {
super(message, ex);
}
}
将这个类继承于EJBException,可以被Backing Bean捕获。
已修改的异常
import jakarta.ejb.EJBException;
public class ChangedException extends EJBException {
private static final long serialVersionUID = -1013228368211446590L;
public ChangedException() {
super();
}
public ChangedException(String message) {
super(message);
}
public ChangedException(Exception ex) {
super(ex);
}
public ChangedException(String message, Exception ex) {
super(message, ex);
}
}
检查版本抛出异常
修改前进行加锁
/**
* LockModeType.PESSIMISTIC_FORCE_INCREMENT 这是排他锁。当使用排他锁时,不能再被编辑,但是可以直接删除。
*
* @param id
* @return
*/
public T edit(Long id) {
return getEntityManager().find(entityClass, id, LockModeType.OPTIMISTIC);
}
修改前和更新前进行版本检查。版本检查依赖于实体是否有属性version。
@Override
public Warehouse edit(Long id) {
Warehouse persistedEntity = em.find(Warehouse.class, id);
if (persistedEntity == null) {
throw new DeletedException("数据已被删除");
}
return super.edit(id);
}
@Override
public Warehouse update(Warehouse entity) throws OptimisticLockException {
Warehouse persistedEntity = em.find(Warehouse.class, entity.getId());
if (persistedEntity == null) {
throw new DeletedException("数据已被删除");
}
if (persistedEntity != null && !persistedEntity.getVersion().equals(entity.getVersion())) {
throw new ChangedException("数据已被修改");
}
return super.update(entity);
}
若实体不存在抛出异常,若实体版本不同也抛出异常。
在Backing Bean中捕获异常
Backing Bean是与视图层交互的逻辑层,应该所有的异常的在这里被捕获并转换为好友的操作提示。
public void edit(Long id) {
try {
this.current = warehouseBean.edit(id);
PrimeFaces.current().executeScript("PF('manageWarehouseDialog').show()");
} catch (DeletedException ex) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "提示", "数据已被另一用户删除,不可修改"));
PrimeFaces.current().ajax().update("form:messages", "form:dt-warehouses");
}
}
public void save() {
if (this.current.getId() == null) {
try {
warehouseBean.save(this.current);
this.data = warehouseBean.getJpaLazyDataModel();
LOGGER.log(Level.CONFIG, "已新增仓库:{0}", new Object[]{this.current.getName()});
facesContext.addMessage(null, new FacesMessage("提示", "已新增仓库 " + this.current.getName()));
} catch (EJBException ex) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "告警", "已存在同名的仓库"));
}
} else {
try {
warehouseBean.update(this.current);
LOGGER.log(Level.CONFIG, "已更新仓库:{0}", new Object[]{this.current.getName()});
facesContext.addMessage(null, new FacesMessage("提示", "已更新仓库 " + this.current.getName()));
} catch (ChangedException ex) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "提示", "仓库" + this.current.getName() + "已被另一用户修改,修改失败"));
} catch (DeletedException ex) {
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "提示", "仓库" + this.current.getName() + "已被另一用户删除,修改失败"));
}
}
PrimeFaces.current().executeScript("PF('manageWarehouseDialog').hide()");
PrimeFaces.current().ajax().update("form:messages", "form:dt-warehouses");
}