JPA锁机制概述
数据库并发访问的时候,为了保证操作数据的完整性,往往会对并发数据的更新做出限制。例如,允许一个Session进行更新处理,其他Session必须等此Session更新完成后才可以进行更新处理,这样的机制就称为数据库锁机制。JPA中也支持锁机制处理,且主要支持两类锁。
- 悲观锁(Pessimistic):假设数据访问一直存在并发更新。悲观锁一直都存在,依靠的是数据库的锁机制。
- 乐观锁(Optimistic):假设不会进行数据并发访问(不会同时出现数据更新处理)。乐观锁主要依靠算法来实现。设置一个版本号,通过版本号来判断当前的Session能否进行更新。
要实现JPA锁机制,可以通过EntityManager接口来完成。EntityManager接口定义的数据锁操作如下。
编号 | 方法 | 类型 | 描述 |
---|---|---|---|
1 | public find(Class entityClass,Object primaryKey,LockModeType lockMode) | 普通 | 使用锁模式查询 |
2 | public void lock(Class entityClass,LockModeType lockMode) | 普通 | 为持久化对象设置锁 |
锁处理主要是通过javax.persistence.LockModeType枚举类定义锁模式。
测试时使用的SQL
CREATE TABLE `dept` (
`deptno` bigint NOT NULL AUTO_INCREMENT,
`avgsal` double NOT NULL,
`createdate` date DEFAULT NULL,
`dname` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`num` int NOT NULL,
`vseq` bigint DEFAULT '0' COMMENT '版本号',
PRIMARY KEY (`deptno`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
测试时所用的实体类:
package com.xiyue.leaspring.po;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;
@SuppressWarnings("serial")
@Cacheable(true)
@Entity
@Table(name = "dept")
public class Dept implements Serializable {//持久化类
@Id//主键类
@GeneratedValue(strategy = GenerationType.IDENTITY)//主键生成方式
private Long deptno;//字段映射(属性名称=字段名称)
private double avgsal;
@Temporal(TemporalType.DATE)
private Date createdate;
private String dname;
private int num;
@Version
private Long vseq;
public Long getDeptno() {
return deptno;
}
public void setDeptno(Long deptno) {
this.deptno = deptno;
}
public double getAvgsal() {
return avgsal;
}
public void setAvgsal(double avgsal) {
this.avgsal = avgsal;
}
public Date getCreatedate() {
return createdate;
}
public void setCreatedate(Date createdate) {
this.createdate = createdate;
}
public String getDname() {
return dname;
}
public void setDname(String dname) {
this.dname = dname;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Dept [deptno=" + deptno + ", avgsal=" + avgsal + ", createdate=" + createdate + ", dname=" + dname
+ ", num=" + num + "]";
}
}
悲观锁
悲观锁认为,用户的并发访问会一直发生,因此在整个处理中会采用锁机制对事务内的操作数据进行锁定,这样其他的事务就无法进行该数据的更新操作。
LockModeType枚举类对悲观锁提供了4种处理模式。
- PESSIMISTIC_READ:只要事务读实体,实体管理器就会锁定实体,直到事务完成(提交或回滚)后才解锁处理。这种锁模式不会阻碍其他事务读取数据。
- PESSIMISTIC_WRITE:只要事务更新实体,实体管理器就会锁定实体。这种锁模式强制尝试修改实体数据的事务串行化,当多个并发更新事务出现,导致更新失败几率较高时,使用这种锁模式。
- PESSIMISTIC_FORCE_INCREMENT:事务读实体时,实体管理器就锁定实体。事务结束后,增加实体的版本属性,即使实体未做修改。
- NONE:不使用锁。
/**
* 使用悲观锁。
*/
@Test
public void findPessimismLock() {
JPAEntityFactory.getEntityManager().getTransaction().begin();//开启事务
JPAEntityFactory.getEntityManager().find(Dept.class,3L,
LockModeType.PESSIMISTIC_WRITE);
JPAEntityFactory.getEntityManager().getTransaction().rollback();
}
Hibernate:
select
dept0_.deptno as deptno1_0_0_,
dept0_.avgsal as avgsal2_0_0_,
dept0_.createdate as createda3_0_0_,
dept0_.dname as dname4_0_0_,
dept0_.num as num5_0_0_
from
dept dept0_
where
dept0_.deptno=? for update
查询语句中,使用数据库锁机制FOR UPDATE语句进行了数据锁定,这样当此事务提交或回滚前,该数据都无法被其他Session修改。
乐观锁
JPA早期提供的锁机制是乐观锁。乐观锁通常假设不存在多个事务修改同一条数据的情况。乐观锁需要在数据表上增加一个表示数据版本的编号,进行数据更新时通过此编号判断是否有其他Session进行过数据更新。判断过程如下:
- 第一个Session与第二个Session同时读取一条数据,该数据的版本号为100。
- 第一个Session更新数据时(此时第二个Session未关闭),自动将版本号由100修改为101。
- 第二个Session读取时版本编号为100,保存时数据版本编号为101,由于版本编号不同,无法进行数据更新处理。
对于乐观锁,LockModeType枚举类有如下两种锁处理模式。
- OPTIMISTIC:和READ锁模式相同,JPA 2.0仍然支持READ锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC。
- OPTIMISTIC_FORCE_INCREMENT:和WRITE锁模式相同,JPA 2.0仍然支持WRITE锁模式,在新应用程序中明确推荐使用OPTIMISTIC_FORCE_INCREMENT。
/**
* 定义数据库脚本,为表中追加版本编号
*/
@Test
public void findOptimisticLock() {
JPAEntityFactory.getEntityManager().getTransaction().begin();//开启事务
Dept dept = JPAEntityFactory.getEntityManager().find(Dept.class,3L
,LockModeType.OPTIMISTIC_FORCE_INCREMENT);//乐观锁
dept.setNum(9999);//修改数据
JPAEntityFactory.getEntityManager().getTransaction().commit();//可以回滚或提交
}
Hibernate:
select
dept0_.deptno as deptno1_0_0_,
dept0_.avgsal as avgsal2_0_0_,
dept0_.createdate as createda3_0_0_,
dept0_.dname as dname4_0_0_,
dept0_.num as num5_0_0_,
dept0_.vseq as vseq6_0_0_
from
dept dept0_
where
dept0_.deptno=?
Hibernate:
update
dept
set
avgsal=?,
createdate=?,
dname=?,
num=?,
vseq=?
where
deptno=?
and vseq=?
Hibernate:
update
dept
set
vseq=?
where
deptno=?
and vseq=?
如果此时有多个Session进行数据更新处理,当版本号不匹配时,将出现org.hibernate.StaleStateException异常。