Hibernate Gossip: 乐观锁定(Optimistic locking)
悲观锁定假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其它的客户不能对数据进行存取,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,如果每一次读取数据都造成锁定,其后继的存取就必须等待,这将造成效能上的问题,造成后继使用者的长时间等待。
乐观锁定(Optimisticlocking)则乐观的认为数据的存取很少发生同时存取的问题,因而不作数据库层次上的锁定,为了维护正确的数据,乐观锁定使用应用程序上的逻辑实现版本控制的解决。
在不实行悲观锁定策略的情况下,数据不一致的情况一但发生,有几个解决的方法,一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。
Hibernate中透过版本号检查来实现后更新为主,这也是Hibernate所推荐的方式,在数据库中加入一个version字段记录,在读取数据时连同版本号一同读取,并在更新数据时比对版本号与数据库中的版本号,如果等于数据库中的版本号则予以更新,并递增版本号,如果小于数据库中的版本号就丢出例外。
实际来透过范例了解Hibernate的乐观锁定如何实现,首先在数据库中新增一个表格:
CREATETABLEuser(
idINT(11)NOTNULLauto_incrementPRIMARYKEY,
versionINT,
nameVARCHAR(100)NOTNULLdefault'',
ageINT
);
这个user表格中的version用来记录版本号,以供Hibernate实现乐观锁定,接着设计User类别,当中必须包括version属性:
User.java
packageonlyfun.caterpillar;
publicclassUser{
privateIntegerid;
privateIntegerversion;//增加版本屬性
privateStringname;
privateIntegerage;
publicUser(){
}
publicIntegergetId(){
returnid;
}
publicvoidsetId(Integerid){
this.id=id;
}
publicIntegergetVersion(){
returnversion;
}
publicvoidsetVersion(Integerversion){
this.version=version;
}
publicStringgetName(){
returnname;
}
publicvoidsetName(Stringname){
this.name=name;
}
publicIntegergetAge(){
returnage;
}
publicvoidsetAge(Integerage){
this.age=age;
}
}
在映射文件的定义方面,则如下所示:
User.hbm.xml
<?xmlversion="1.0"encoding="utf-8"?>
<!DOCTYPEhibernate-mapping
PUBLIC"-//Hibernate/HibernateMappingDTD3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<classname="onlyfun.caterpillar.User"
table="user"
optimistic-lock="version">
<idname="id"column="id"type="java.lang.Integer">
<generatorclass="native"/>
</id>
<versionname="version"
column="version"
type="java.lang.Integer"/>
<propertyname="name"column="name"type="java.lang.String"/>
<propertyname="age"column="age"type="java.lang.Integer"/>
</class>
</hibernate-mapping>
注意<version>标签必须出现在<id>卷标之后,接着您可以试着在数据库中新增数据,例如:
Useruser=newUser();
user.setName("caterpillar");
user.setAge(newInteger(30));
Sessionsession=sessionFactory.openSession();
Transactiontx=session.beginTransaction();
session.save(user);
tx.commit();
session.close();
您可以检视数据库中的数据,每一次对同一笔数据进行更新,version字段的内容都会自动更新,接着来作个实验,直接以范例说明:
//有使用1者开启了一个session1
Sessionsession1=sessionFactory.openSession();
//在这之后,马上有另一个使用者2开启了session2
Sessionsession2=sessionFactory.openSession();
Integerid=newInteger(1);
//使用者1查询数据
UseruserV1=(User)session1.load(User.class,id);
//使用者2查询同一笔数据
UseruserV2=(User)session2.load(User.class,id);
//此时两个版本号是相同的
System.out.println("v1v2"+userV1.getVersion().intValue()+""+userV2.getVersion().intValue());
Transactiontx1=session1.beginTransaction();
Transactiontx2=session2.beginTransaction();
//使用者1更新数据
userV1.setAge(newInteger(31));
tx1.commit();
//此时由于数据更新,数据库中的版本号递增了
//两笔数据版本号不一样了
System.out.println("v1v2"+userV1.getVersion().intValue()+""+userV2.getVersion().intValue());
//userV2的age资料还是旧的
//数据更新
userV2.setName("justin");
//因版本号比数据库中的旧
//送出更新数据会失败,丢出StableObjectStateException例外
tx2.commit();
session1.close();
session2.close();
运行以下的程序片段,会出现以下的结果:
Hibernate:
selectuser0_.idasid0_,user0_.versionasversion0_0_,user0_.nameas
name0_0_,user0_.ageasage0_0_fromuseruser0_whereuser0_.id=?
Hibernate:
selectuser0_.idasid0_,user0_.versionasversion0_0_,user0_.nameas
name0_0_,user0_.ageasage0_0_fromuseruser0_whereuser0_.id=?
v1v200
Hibernate:updateusersetversion=?,name=?,age=?whereid=?andversion=?
v1v210
Hibernate:updateusersetversion=?,name=?,age=?whereid=?andversion=?
16:11:43,187ERRORAbstractFlushingEventListener:277-Couldnotsynchronizedatabasestatewithsession
org.hibernate.StaleObjectStateException:
Rowwasupdatedordeletedbyanothertransaction(orunsaved-value
mappingwasincorrect):[onlyfun.caterpillar.User#1]
atorg.hibernate.persister.entity.BasicEntityPersister.check(BasicEntityPersister.java:1441)
由于新的版本号是1,而userV2的版本号还是0,因此更新失败丢出StableObjectStateException,您可以捕捉这个例外作善后处理,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据秀出来,让使用者有机会比对不一致的数据,以决定要变更的部份,或者您可以设计程序自动读取新的数据,并比对真正要更新的数据,这一切可以在背景执行,而不用让您的使用者知道。
要注意的是,由于乐观锁定是使用系统中的程序来控制,而不是使用数据库中的锁定机制,因而如果有人特意自行更新版本讯息来越过检查,则锁定机制就会无效,例如在上例中自行更改userV2的version属性,使之与数据库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于数据是由外部系统而来,因而版本信息不受控制时,锁定机制将会有问题,设计时必须注意。