首先我们需要对Hibernate中的事务,以及并发所造成的问题进行一个了解,有一篇博客讲述的非常全在此就不赘述,请提前参考《Hibernate事务与并发问题处理(乐观锁与悲观锁)》,下面只记录我认为重点的地方。
使用乐观锁解决事务并发问题
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。
乐观锁的工作原理:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
Hibernate为乐观锁提供了3中实现:
注意:不建议在新的应用程序中定义没有版本或者时间戳列的版本控制:它更慢,更复杂,如果你正在使用脱管对象,它则不会生效。
注意:参考资料中使用的是配置文件的方式来配置Hibernate,不过我对其进行修正采用注解的方式,如果想要参考配置文件的方式可查看博客最下方的参考资料。
create table studentVersion (id varchar(32),name varchar(32),ver int);
注:其中ver保存的就是版本。
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
@Entity
@Table(name = "studentversion")
public class Student {
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
@Column(columnDefinition = "CHAR(32)")
@Id
private String id;
private String name;
@Version
@Column(name="ver")
private int version;
//省略所有getter和setter方法
...
}
注意:上面主键的生成策略时UUID,但是其使用的是org.hibernate.id.UUIDGenerator,我们将其改为
注意:参考资料《Hibernate和UUID标示符》修正——其中UUIDGenerator生成策略生成的UUID的长度为36位,参考资料中是16位,上面的代码已经修正,这一点需要注意。
在hibernate.cfg.xml文件中添加如下配置:
public void test() {
// 使用默认的hibernate.cfg.xml配置文件创建Configuration实例
Configuration cfg = new Configuration().configure();
// 以Configuration实例来创建SessionFactory实例
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(cfg.getProperties())
.build();
SessionFactory sessionFactory = cfg.buildSessionFactory(serviceRegistry);
Session session = sessionFactory.openSession();
Transaction t = session.beginTransaction();//开启事务
Student stu = new Student();
stu.setName("tom11");
session.save(stu);
t.commit();
/*
* 模拟多个session操作student数据表
*/
Session session1 = sessionFactory.openSession();
Session session2 = sessionFactory.openSession();
Student stu1 = (Student) session1.createQuery("from Student s where s.name='tom11'").uniqueResult();
Student stu2 = (Student) session2.createQuery("from Student s where s.name='tom11'").uniqueResult();
// 这时候,两个版本号是相同的
System.out.println("v1=" + stu1.getVersion() + "--v2=" + stu2.getVersion());
Transaction tx1 = session1.beginTransaction();
stu1.setName("session1");
tx1.commit();
// 这时候,两个版本号是不同的,其中一个的版本号递增了
System.out.println("v1=" + stu1.getVersion() + "--v2=" + stu2.getVersion());
Transaction tx2 = session2.beginTransaction();
stu2.setName("session2");
tx2.commit();
}
运行结果如下:
1494440085669|1|statement|connection 0|insert into studentversion (name, ver, id) values (?, ?, ?)|insert into studentversion (name, ver, id) values ('tom11', 0, '5bbc958a-c921-4740-8cfd-127593cf3048')
1494440085825|153|commit|connection 0||
1494440086027|6|statement|connection 2|select student0_.id as id1_3_, student0_.name as name2_3_, student0_.ver as ver3_3_ from studentversion student0_ where student0_.name='tom11'|select student0_.id as id1_3_, student0_.name as name2_3_, student0_.ver as ver3_3_ from studentversion student0_ where student0_.name='tom11'
1494440086050|1|statement|connection 1|select student0_.id as id1_3_, student0_.name as name2_3_, student0_.ver as ver3_3_ from studentversion student0_ where student0_.name='tom11'|select student0_.id as id1_3_, student0_.name as name2_3_, student0_.ver as ver3_3_ from studentversion student0_ where student0_.name='tom11'
v1=0--v2=0
1494440086058|1|statement|connection 2|update studentversion set name=?, ver=? where id=? and ver=?|update studentversion set name='session1', ver=1 where id='5bbc958a-c921-4740-8cfd-127593cf3048' and ver=0
1494440086106|45|commit|connection 2||
v1=1--v2=0
1494440086108|0|statement|connection 1|update studentversion set name=?, ver=? where id=? and ver=?|update studentversion set name='session2', ver=1 where id='5bbc958a-c921-4740-8cfd-127593cf3048' and ver=0
Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.crazyit.app.domain.Student#5bbc958a-c921-4740-8cfd-127593cf3048]
at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2541)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3285)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3183)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3525)
at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:159)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:463)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:349)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1222)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:425)
at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177)
at lee.Test.main(Test.java:47)
分析结果:
由于Hibernate生成的SQL语句占用的篇幅比较大,而且显示不了详细信息所以我禁止其显示,采用P6Spy进行显示SQL语句,异常之前比较长的几行就是其显示信息。
可以看到,第二个“用户”session2修改数据时候,记录的版本号已经被session1更新过了,所以抛出了红色的异常,我们可以在实际应用中处理这个异常,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据展示出来,让使用者有机会比较一下,或者设计程序自动读取新的数据
注意:
要注意的是,由于乐观锁定是使用系统中的程式来控制,而不是使用资料库中的锁定机制,因而如果有人特意自行更新版本讯息来越过检查,则锁定机制就会无效,例如在上例中自行更改stu的version属性,使之与资料库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于资料是由外部系统而来,因而版本资讯不受控制时,锁定机制将会有问题,设计时必须注意。
如果手工设置stu.setVersion()自行更新版本以跳过检查,则这种乐观锁就会失效,应对方法可以将Student.Java的setVersion设置成private
————》》》基于timestamp(时间戳)的实现待续
参考资料: