ITEEDU

Hibernate Gossip: 乐观锁定(Optimistic locking)

悲观锁定假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其它的客户不能对数据 进行存取,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,如果每一次读取数据都造成锁定,其后继的存取就必须等 待,这将造成效能上的问题,造成后继使用者的长时间等待。

乐观锁定(Optimistic locking)则乐观的认为数据的存取很少发生同时存取的问题,因而不作数据库层次上的锁定,为了维护正确的数据,乐观锁定使用应用程序上的逻辑实现版本控制的解决。

在不实行悲观锁定策略的情况下,数据不一致的情况一但发生,有几个解决的方法,一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。

Hibernate中透过版本号检查来实现后更新为主,这也是Hibernate所推荐的方式,在数据库中加入一个version字段记录,在读取数据时 连同版本号一同读取,并在更新数据时比对版本号与数据库中的版本号,如果等于数据库中的版本号则予以更新,并递增版本号,如果小于数据库中的版本号就丢出 例外。

实际来透过范例了解Hibernate的乐观锁定如何实现,首先在数据库中新增一个表格:
CREATE TABLE user (
    id INT(11) NOT NULL auto_increment PRIMARY KEY,
    version INT,
    name VARCHAR(100) NOT NULL default '',
    age INT
);
这个user表格中的version用来记录版本号,以供Hibernate实现乐观锁定,接着设计User类别,当中必须包括version属性:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private Integer version; // 增加版本屬性
private String name;
private Integer age;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getVersion() {
return version;
}

public void setVersion(Integer version) {
this.version = version;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}
在映射文件的定义方面,则如下所示:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

<class name="onlyfun.caterpillar.User"
table="user"
optimistic-lock="version">

<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>

<version name="version"
column="version"
type="java.lang.Integer"/>

<property name="name" column="name" type="java.lang.String"/>

<property name="age" column="age" type="java.lang.Integer"/>

</class>

</hibernate-mapping>
注意<version>标签必须出现在<id>卷标之后,接着您可以试着在数据库中新增数据,例如:
User user = new User(); 
user.setName(  "caterpillar"); 
user.setAge(new Integer(30)); 
Session session = sessionFactory.openSession(); 
Transaction tx =    session.beginTransaction();
session.save(user); 
tx.commit(); 
session.close(); 
您可以检视数据库中的数据,每一次对同一笔数据进行更新,version字段的内容都会自动更新,接着来作个实验,直接以范例说明:
// 有使用1者开启了一个session1
Session session1 = sessionFactory.openSession(); 
// 在这之后,马上有另一个使用者2开启了session2 
Session session2 = sessionFactory.openSession(); 

Integer id = new Integer(1); 

// 使用者1查询数据  
User userV1 = (User) session1.load(User.class, id); 
// 使用者2查询同一笔数据 
User userV2 = (User) session2.load(User.class, id); 

// 此时两个版本号是相同的 
System.out.println(  " v1 v2 "+ userV1.getVersion().intValue() + " "  + userV2.getVersion().intValue());

Transaction tx1 = session1.beginTransaction(); 
Transaction tx2 = session2.beginTransaction(); 

// 使用者1更新数据
userV1.setAge(new Integer(31)); 
tx1.commit(); 

// 此时由于数据更新,数据库中的版本号递增了 
// 两笔数据版本号不一样了 
System.out.println(  " v1 v2 "+ userV1.getVersion().intValue() + " " + userV2.getVersion().intValue());

// userV2 的 age 资料还是旧的 
//   数据更新
userV2.setName(  "justin");
// 因版本号比数据库中的旧 
// 送出更新数据会失败,丢出StableObjectStateException例外 
tx2.commit(); 

session1.close(); 
session2.close();
运行以下的程序片段,会出现以下的结果:
Hibernate:
select user0_.id as id0_, user0_.version as version0_0_, user0_.name as
name0_0_, user0_.age as age0_0_ from user user0_ where user0_.id=?
Hibernate:
select user0_.id as id0_, user0_.version as version0_0_, user0_.name as
name0_0_, user0_.age as age0_0_ from user user0_ where user0_.id=? 
 v1 v2 0 0
Hibernate: update user set version=?, name=?, age=? where id=? and version=? 
 v1 v2 1 0
Hibernate: update user set version=?, name=?, age=? where id=? and version=? 
16:11:43,187 ERROR AbstractFlushingEventListener:277 - Could not synchronize database state with session 
org.hibernate.StaleObjectStateException:
Row was updated or deleted by another transaction (or unsaved-value
mapping was incorrect): [onlyfun.caterpillar.User#1] 
    at org.hibernate.persister.entity.BasicEntityPersister.check(BasicEntityPersister.java:1441)
由于新的版本号是1,而userV2的版本号还是0,因此更新失败丢出StableObjectStateException,您可以捕捉这个例外作善后 处理,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据秀出来,让使用者有机会比对不一致的数据,以决定要变更的部份,或者您可以 设计程序自动读取新的数据,并比对真正要更新的数据,这一切可以在背景执行,而不用让您的使用者知道。

要注意的是,由于乐观锁定是使用系统中的程序来控制,而不是使用数据库中的锁定机制,因而如果有人特意自行更新版本讯息来越过检查,则锁定机制就会无效, 例如在上例中自行更改userV2的version属性,使之与数据库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于数据是由外部系统而 来,因而版本信息不受控制时,锁定机制将会有问题,设计时必须注意。