乐观锁是由应用程序提供的一种机制,这种机制既能保证多个事务并发访问数据,又能防止第二类丢失更新问题。在应用程序中,可以利用Hibernate提供的版本控制功能来实现乐观锁。既可以用一个递增的整数来表示版本号,也可以用时间戳来表示版本号,跟踪数据库表中记录的版本。
下面介绍利用整数类型的版本控制属性对ACCOUNTS表中记录进行版本控制的步骤。
(1)在Account类中定义一个代表版本信息的version属性,这个属性用@Version注解来标识:
@Version
@Column(name="VERSION")
private int version;
public int getVersion() {
return this.version;
}
public void setVersion(int version) {
this.version = version;
}
(2)在ACCOUNTS表中定义一个代表版本信息的字段:
create table ACCOUNTS (
ID bigint not null,
NAME varchar(15),
BALANCE decimal(10,2),
VERSION integer,
primary key (ID)
) engine=INNODB;
以下transferCheck()实现转账事务,withdraw()方法实现取款事务。这两个方法都采用LockModeType.OPTIMISTIC乐观锁,并且都会处理javax.persistence.OptimisticLockException异常:
public void transferCheck() throws Exception{
EntityManager entityManager =
entityManagerFactory.createEntityManager();
EntityTransaction tx = null;
try {
tx = entityManager.getTransaction();
tx.begin();
log.write("transferCheck():开始事务");
Thread.sleep(500);
Account account=entityManager.find(Account.class,
Long.valueOf(1),LockModeType.OPTIMISTIC);
log.write("transferCheck():查询到存款余额为:balance="
+account.getBalance());
Thread.sleep(500);
account.setBalance(account.getBalance()+100);
log.write("transferCheck():汇入100元,把存款余额改为:"
+account.getBalance());
tx.commit();
log.write("transferCheck():提交事务");
Thread.sleep(500);
}catch (RuntimeException e) {
if(e instanceof RollbackException &&
e.getCause() instanceof OptimisticLockException){
System.out.println("账户信息已被其他事务修改,本事务被撤销,"
+"请重新开始支票转账事务");
log.write("transfter():账户信息已被其他事务修改,本事务被撤销");
}
if (tx != null) {
tx.rollback();
}
throw e;
} finally {
entityManager.close();
}
}
以上transferCheck()方法加载Account对象时,显式指定使用乐观锁。实际上,只要在Account类中通过@Version注解设置了版本控制属性,那么加载Account对象时,默认情况下会使用乐观锁。因此以下两段代码的作用是等价的:
//显式指定使用乐观锁
Account account=entityManager.find(Account.class,
Long.valueOf(1),LockModeType.OPTIMISTIC);
或者:
//当Account类中设置了版本控制属性,默认情况下使用乐观锁
Account account=entityManager.find(Account.class, Long.valueOf(1));
接下来介绍加入版本控制后Hibernate的运行时行为。BusinessService类的main()方法先调用registerAccount()方法持久化一个Account对象:
tx = entityManager.getTransaction();
tx.begin();
Account account=new Account();
account.setName("Tom");
account.setBalance(1000);
entityManager.persist(account);
tx.commit();
应用程序无需为Account对象的version属性显式赋值,在持久化Account对象时,Hibernate会自动为它赋初始值为0,Hibernate执行的insert语句为:
insert into ACCOUNTS values(1, 'Tom',1000,0);
当Hibernate加载一个Account对象时,它的version属性表示ACCOUNTS表中相关记录的版本。当Hibernate更新一个Account对象时,会根据Session的持久化缓存中Account对象的id与version属性的当前值到ACCOUNTS表中去定位匹配的记录,假定Session缓存中Account对象的version属性为0,那么在取款事务中Hibernate执行的update语句为:
update ACCOUNTS set NAME=’Tom’,BALANCE=900,VERSION=1
where ID=1 and VERSION=0;
如果存在匹配的记录,就更新这条记录,并且把VERSION字段的值增加为1,此外,还会把Session缓存中Account对象的version属性也更新为1。当支票转账事务接着执行以下update语句时:
update ACCOUNTS set NAME=’Tom’,BALANCE=1100,VERSION=1
where ID=1 and VERSION=0;
由于ID为1的ACCOUNTS记录的版本已经被取款事务修改,因此找不到匹配的记录,此时Hibernate会抛出StaleObjectStateException,接着JPA把它包装为OptimisticLockException,再把它包装为RollbackException,再把它抛出。
在应用程序中,应该处理该OptimisticLockException异常,这种异常有两种处理方式:
- 方式一:自动撤销事务,通知用户账户信息已被其他事务修改,需要重新开始事务。本例程就采用这种方式。
- 方式二:通知用户账户信息已被其他事务修改,显示最新存款余额信息,由用户决定如何继续事务,用户也可以决定立刻撤销事务。