Hibernate JPA 锁机制
当数据库并发访问的时候为了保证操作的一致性,那么往往会对并发数据的访问做出限制,例如:只允许一个会话处理,这样的机制就称为锁机制,而在JPA
之中也支持锁机制的处理,而JPA
支持两类锁:
- 悲观锁:假设数据的访问一直都存在有并发,所以悲观锁一直都会存在,主要依靠的是数据库的锁机制
- 乐观锁:假设不会进行并发访问(不会出现同时的数据更新处理)主要是依靠算法来实现的,设置版本号,通过版本号来判断当前的
Session
能否进行更新
在JPA
里面专门提供有一个锁的处理模式:javax.persistence.LockModeType;
1、悲观锁:Pessimistic
悲观锁认为用户的并发访问会一直发生,并且在整个的处理之中悲观锁一定会采用锁的机制,对一个事务内的操作数据进行锁定,这样其他的事务就无法进行该数据的更新操作了。在悲观锁中定义了如下几种处理模式:
-
NONE:不适用锁
-
PESSIMISTIEC_READ:只要事务读实体,实体管理器就锁定实体,直到事务完成锁才会解开,当你想使用重复语义查询数据时使用这种锁模式,换句话说,当你想确保数据在连续读取期间不被修改,这种锁模式不会阻碍其他事务读取数据
-
PESSIMISTIC_WRITE:只要事务更新实体,实体管理器就会锁定实体,这种锁模式强制尝试修改实体数据的事务串行化,当多个并发更新事务出现更新失败几率较高时使用这种锁模式
-
PESSIMISTIC_FORCE_INCREMENT:当事务读实体时,实体管理器就锁定实体,当事务结束时会增加实体的版本属性,即使实体没有修改
1、使用悲观锁编写测试类
/**
* 悲观锁
*/
@Test
public void testFindPessimisticLock(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin(); // 开启事务
// 加上写入悲观锁
entityManager.find(Customer.class, 2L, LockModeType.PESSIMISTIC_WRITE);
entityManager.getTransaction().rollback(); // 可以回滚或提交
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
tb_customer customer0_
where
customer0_.id=? for update
可以发现查询语句最后跟上了:for update
(代表我对这条数据进行锁定了,在我没有提交或回滚事务之前,其他线程不能对该数据进行修改)
2、模仿两个线程来进行锁处理。模拟场景:
- 线程A查询数据,并给这条数据加上写入悲观锁,然后对数据进行修改,在事务提交之前休眠20秒
- 线程B查询同一条数据,并给这条数据加上写入悲观锁,然后对数据进行修改,不休眠直接提交事务
线程A:TestPessimisticA.java
public class TestPessimisticA {
/**
* 悲观锁
*/
public static void main(String[] args) throws Exception {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入悲观锁
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.PESSIMISTIC_WRITE);
customer.setName("悲观锁修改A");
TimeUnit.SECONDS.sleep(20);// 休眠20秒
entityManager.getTransaction().commit();// 可以提交或回滚
entityManager.close();
}
线程B:TestPessimisticB.java
public class TestPessimisticB {
/**
* 悲观锁
*/
public static void main(String[] args) {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入悲观锁
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.PESSIMISTIC_WRITE);
customer.setName("悲观锁修改B");
entityManager.getTransaction().commit();// 可以提交或回滚
entityManager.close();
}
操作步骤和结论:
- 先执行线程A,然后里面去执行线程B
- 会出现线程A执行完数据修改后提交事务前,休眠20秒,控制台停住了
- 线程B虽然没有休眠,可是也依旧停住了,因为在等待线程A操作完该数据
- 等待线程A操作完数据提交了事务,线程B也立马提交了事务
- 最终这条数据经历了先被A修改,然后立马又被线程B修改了
查看日志(从日志上什么也看不出来,线程A和B输出日志一摸一样,需要模拟时看控制台停滞与停止状态)
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=? for update
Hibernate:
update
tb_customer
set
customer_address=?,
customer_age=?,
customer_name=?,
customer_phone=?,
customer_sex=?
where
customer_id=?
这个必须要亲自模拟多次才能理解清楚,可以在线程B加上控制台输出
2、乐观锁:Optimistic
JPA
最早的时候所提供的锁机制就是乐观锁,乐观锁:假设没有多个事务修改同一条数据的情况,而且乐观锁最大的差别就是需要对数据表上增加一个表示数据版本的编号。对于乐观锁有如下几种锁的处理模式:
- OPTIMISTIC:它和READ锁模式相同,
JPA 2.0
仍然支持READ
模式,但明确指出在新应用程序中推荐使用OPTIMISTIC
- OPTIMISTIC_FORCE_INCREMENT:它和WRITE锁模式相同,
JPA 2.0
仍然支持WRITE
锁模式,但明确的指出在新应用程序中推荐使用OPTIMISTIC_FORCE_INCREMENT
1、修改数据库脚本:增加一个版本号字段:c_version
(如果JPA
设置自动检测修改表的话可以忽略这一步)
alter table tb_customer add column customer_version bigint default 0;
2、修改实体类,增加版本号字段
@Version
@Column(name = "customer_version",columnDefinition="bigint default 0")
private Long version;
3、采用乐观锁操作,编写程序类
/**
* 乐观锁
*/
@Test
public void testFindOptimisticLock(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
Customer customer = entityManager.find(Customer.class, 2L, LockModeType.OPTIMISTIC_FORCE_INCREMENT);// 查询并加上写入乐观锁
customer.setName("李四四");
entityManager.getTransaction().commit();// 可以回滚或提交
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_,
customer0_.c_version as c_versio7_0_0_
from
c_customer customer0_
where
customer0_.id=?
Hibernate: // 会发现每一次在进行更新的时候会出现版本号的修改操作
update
tb_customer
set
customer_version=?
where
customer_id=?
and customer_version=?
4、模拟多用户并发访问。模拟场景(与悲观锁场景一样):
- 线程A查询数据,并给这条数据加上写入乐观锁,然后对数据进行修改,在事务提交之前休眠20秒
- 线程B查询同一条数据,并给这条数据加上写入悲观锁,然后对数据进行修改,不休眠直接提交事务
线程A:TestOptimisticA.java
public class TestOptimisticA {
/**
* 乐观锁
* @param args
*/
public static void main(String[] args) throws Exception {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.OPTIMISTIC_FORCE_INCREMENT);// 加上写入乐观锁
customer.setName("乐观锁修改A");
TimeUnit.SECONDS.sleep(20);// 休眠20秒
entityManager.getTransaction().commit();// 可以提交或回滚
entityManager.close();
}
线程B:TestOptimisticB.java
/**
* 乐观锁
* @param args
*/
public static void main(String[] args) {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
System.err.println("查询数据--------");
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.OPTIMISTIC_FORCE_INCREMENT);// 加上写入乐观锁
customer.setName("乐观锁修改B");
entityManager.getTransaction().commit();// 可以提交或回滚
System.err.println("修改线程B的数据完成");
entityManager.close();
}
操作步骤和结论:
- 先执行线程A,然后里面去执行线程B
- 由于乐观锁不是对一条数据的锁定,等于现在第二个事务(线程B)会先实现数据的更新
- 但是由于第一个事务先启动,所以他读取到版本号和它更新时候的版本号肯定是不同的
- 所以在线程A提交事务的时候会报如下错误:
Caused by: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [domain.Customer#14]
....
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [domain.Customer#14]
乐观锁是一种基于数据算法的锁的处理机制,乐观锁特点就是在于你的项目之中不存在多个用户更新同一数据的情况。如果一直存在并发更新同一数据的话,那么一定采用悲观锁