Hibernate中的事务管理以及缓存管理
l 事务的概念(前面已经讲述)
l Hibernate中的Session缓存(前面已经讲述)
l Hibernate中的二级缓存以及二级缓存的缓存策略
l Query查询缓存
l Query.list ()、iterator()深入
l 悲观锁定(Pessimistic Locking)
l 乐观锁定(Optimistic Locking)
二级缓存
前面,我们讲述过Session级别的缓存。
Session级别的缓存,它的生命周期与Session的生命周期一致。当Session关闭时,其缓存将失效。
这里,进一步讨论Hibernate提供的另一种缓存机制-二级缓存,它比Session级别的缓存更高一级,是属于SessionFactory级别的缓存,该缓存将被属于同一个SessionFactory级别的所有Session共享。
二级缓存为每个类(或集合),配置集群、或JVM级别(SessionFactory级别)的缓存。 你甚至可以为之插入一个集群的缓存。注意,缓存永远不知道其他应用程序对持久化仓库(数据库)可能进行的修改 (即使可以将缓存数据设定为定期失效)。
Hibernate的 Session level 缓存 随着Session生命周期起始与消灭。
以 第一个 Hibernate 中的范例来说,在未使用二级缓存的情况下,如果使用以下的程序片段来查询数据:
Session session = sessionFactory.openSession();
User user1 = (User) session.load(User.class, new Integer(1));
user1.getName();
session.close();
session = sessionFactory.openSession();
User user2 = (User) session.load(User.class, new Integer(1));
user2.getName();
session.close();
则Hibernate将会使用以下的SQL来进行数据查询:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_, user0_.age as age0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id0_, user0_.name as name0_0_, user0_.age as age0_0_ from user user0_ where user0_.id=?
由于Session被关闭,Session level无法起作用,所以第二次的查询仍必须向数据库直接查询。
Hibernate二级缓存可以跨越数个Session,二级缓存由同一个SessionFactory所建立的Session所共享,因而又称为 SessionFactory level缓存。
Hibernate本身并未提供二级缓存的实现,而是藉由第三方(Third-party)产品来实现,Hibernate预设使用EHCache作为其二级缓存的实现,在最简单的情况下,您只需在Hibernate下撰写一个ehcache.xml作为EHCache的资源定义档,可以在 Hibernate下载档案中的etc目录下找到一个已经撰写好的ehcache.xml,以下撰写最简单的ehcache.xml:
ehcache.xml
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
</ehcache>
将这个文件放在Hibernate项目Classpath可存取到的路径下,接着重新运行上面的程序片段,您可以发现Hibernate将使用以下的SQL进行查询:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_, user0_.age as age0_0_ from user user0_ where user0_.id=?
二级缓存被同一个SessionFactory所建立的Session实例所共享,所以即使关闭了Session,下一个Session仍可使用二级缓存,在查询时,Session会先在Session level缓存中查询看有无数据,如果没有就试着从二级缓存中查询数据,查到数据的话就直接返回该条数据,所以在上例中,第二次无需再向数据库进行SQL 查询。
如果打算清除二级缓存的资料,可以使用SessionFactory的evict()方法,例如:
sessionFactory.evict(User.class, user.getId());
如果打算在Hibernate中使用其它第三方产品进行缓存,则可以在hibernate.cfg.xml中定义 hibernate.cache.provider_class属性,例如:
hibernate.cfg.xml
<hibernate-configuration>
<session-factory>
....
<property name="hibernate.cache.provider_class">
org.hibernate.cache.HashtableCacheProvider
</property>
....
</session-factory>
</hibernate-configuration>
通过在hibernate.cache.provider_class属性中指定org.hibernate.cache.CacheProvider的某个实现的类名,你可以选择让Hibernate使用哪个缓存实现。Hibernate打包一些开源缓存实现,提供对它们的内置支持(见下表)。除此之外,你也可以实现你自己的实现,将它们插入到系统中。注意,在3.2版本之前,默认使用EhCache 作为缓存实现,但从3.2起就不再这样了。
HashtableCache是Hibernate自己所提供的二级缓存实现,不过性能与功能上有限,只用于开发时期的测试之用。
可以在映射文件中指定缓存策略,使用<cache>卷标在映像实体或Collection上设定缓存策略,例如:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.User" table="user">
<cache usage="read-only"/>
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>
<property name="name" column="name" type="java.lang.String"/>
<property name="age" column="age" type="java.lang.Integer"/>
</class>
</hibernate-mapping>
类或者集合映射的“<cache>元素”可以有下列形式:
<cache
usage="transactional|read-write|nonstrict-read-write|read-only"
region="RegionName"
include="all|non-lazy"
/>
usage(必须)说明了缓存的策略: transactional、 read-write、 nonstrict-read-write或 read-only。
region (可选, 默认为类或者集合的名字(class or collection role name)) 指定第二级缓存的区域名(name of the second level cache region)
include (可选,默认为 all) non-lazy 当属性级延迟抓取打开时, 标记为lazy="true"的实体的属性可能无法被缓存
另外(首选?), 你可以在hibernate.cfg.xml中指定<class-cache>和 <collection-cache> 元素。
这里的usage 属性指明了缓存并发策略(cache concurrency strategy)。
可以设定的策略包括read-only、read-write、nonstrict-read-write与transactional,并不是每一个第三方缓存实现都支持所有的选项,每一个选项的使用时机与支持的产品,可以直接参考Hibernate官方参考手册的 20.2. The Second Level Cache
只读缓存(Strategy: read only)
如果你的应用程序只需读取一个持久化类的实例,而无需对其修改, 那么就可以对其进行只读 缓存。这是最简单,也是实用性最好的方法。甚至在集群中,它也能完美地运作。
<class name="eg.Immutable" mutable="false">
<cache usage="read-only"/>
....
</class>
读/写缓存(Strategy: read/write)
如果应用程序需要更新数据,那么使用读/写缓存 比较合适。 如果应用程序要求“序列化事务”的隔离级别(serializable transaction isolation level),那么就决不能使用这种缓存策略。 如果在JTA环境中使用缓存,你必须指定hibernate.transaction.manager_lookup_class属性的值, 通过它,Hibernate才能知道该应用程序中JTA的TransactionManager的具体策略。 在其它环境中,你必须保证在Session.close()、或Session.disconnect()调用前, 整个事务已经结束。 如果你想在集群环境中使用此策略,你必须保证底层的缓存实现支持锁定(locking)。Hibernate内置的缓存策略并不支持锁定功能。
<class name="eg.Cat" .... >
<cache usage="read-write"/>
....
<set name="kittens" ... >
<cache usage="read-write"/>
....
</set>
</class>
非严格读/写缓存(Strategy: nonstrict read/write)
如果应用程序只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离, 那么比较适合使用非严格读/写缓存策略。如果在JTA环境中使用该策略, 你必须为其指定hibernate.transaction.manager_lookup_class属性的值, 在其它环境中,你必须保证在Session.close()、或Session.disconnect()调用前, 整个事务已经结束。
事务缓存(transactional)
Hibernate的事务缓存策略提供了全事务的缓存支持, 例如对JBoss TreeCache的支持。这样的缓存只能用于JTA环境中,你必须指定 为其hibernate.transaction.manager_lookup_class属性。
没有一种缓存提供商能够支持上列的所有缓存并发策略。下表中列出了各种提供器、及其各自适用的并发策略。
Query 查询缓存
Hibernate的 Session level 缓存 会在使用Session的load()方法时起作用,在设定条件进行查询时,无法使用缓存的功能,现在考虑一种情况,您的数据库表格中的数据很少变动,在使用Query查询数据时,如果表格内容没有变动,您希望能重用上一次查询的结果,除非表格内容有变动才向数据库查询。
您可以开启Query的缓存功能,因为要使用Query的缓存功能必须在两次查询时所使用的SQL相同,且两次查询之间表格没有任何数据变动下才有意义,所以Hibernate预设是关闭这个功能的,如果您觉得符合这两个条件,那么可以试着开启Query缓存功能来看看效能上有无改进。
先来看看下面的查询程序片段:
Session session = sessionFactory.openSession();
String hql = "from User";
Query query = session.createQuery(hql);
List users = query.list();
for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}
query = session.createQuery(hql);
users = query.list();
for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}
session.close();
在不启用Query缓存的情况下,Hibernate会使用两次SQL向数据库查询数据:
Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_ from user user0_
zhangy
hhp
Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_ from user user0_
zhangy
hhp
如果打算启用Query缓存功能,首先在hibernate.cfg.xml中设定hibernate.cache.use_query_cache属性:
hibernate.cfg.xml
<hibernate-configuration>
<session-factory>
....
<property name="hibernate.cache.use_query_cache">true</property>
....
</session-factory>
</hibernate-configuration>
然后在每次建立Query实例时,执行setCacheable(true):
Session session = sessionFactory.openSession();
String hql = "from User";
Query query = session.createQuery(hql);
// 使用Query缓存
query.setCacheable(true);
List users = query.list();
for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}
query = session.createQuery(hql);
// 使用Query缓存
query.setCacheable(true);
users = query.list();
for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}
session.close();
Hibernate在启用Query缓存后,会保留执行过的查询SQL与查询结果,在下一次查询时会看看SQL是否相同,并看看对应的数据库表格是否有变动(Update/Delete/Insert),如果SQL相同且数据库也没有变动,则将Query缓存中的查询结果返回,上面的程序片段将使用一次 SQL查询,第二次查询时直接返回缓存中的结果.
Query上有list()与iterate()方法,两者的差别在于开启Query缓存之后,list()方法在读取数据时,会利用到Query缓存,而iterate()则不会使用到Query缓存功能,而是直接从数据库中再查询数据。
来看看下面的程序:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from User");
query.setCacheable(true);
List users = query.list();
users = query.list();
session.close();
这个程序片段会使用一次SQL来查询数据库,第二次直接从Query快取中取得数据:
Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_ from user user0_
使用iterate()方法时不会使用到Query快取,例如:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from User");
query.setCacheable(true);
Iterator users = query.iterate();
users = query.iterate();
session.close();
这个程序片段会使用两次SQL向数据库查询:
Hibernate: select user0_.id as col_0_0_ from user user0_
Hibernate: select user0_.id as col_0_0_ from user user0_
悲观锁定
在多个客户端可能读取同一条数据或同时更新一条数据的情况下,必须要有访问控制的手段,防止同一个数据被修改而造成混乱,最简单的手段就是对数据进行锁定,在自己进行数据读取或更新等动作时,锁定其它客户端不能对同一条数据进行任何的动作。
悲观锁定(Pessimistic Locking)一如其名称所示,悲观的认定每次资料存取时,其它的客户端也会存取同一条数据,因此对该条数据进行锁定,直到自己操作完成后解除锁定。
悲观锁定通常通过系统或数据库本身的功能来实现,依赖系统或数据库本身提供的锁定机制,Hibernate即是如此,可以利用Query或 Criteria的setLockMode()方法来设定要锁定的表或列(Row)及其锁定模式,可设定的锁定模式有以下的几个:
LockMode.UPGRADE:利用数据库的for update子句进行锁定。
LockMode.UPGRADE_NOWAIT:使用for update nowait子句进行锁定,在Oracle数据库中使用。
一个设定锁定的例子如下:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from User user");
query.setLockMode("user", LockMode.UPGRADE);
List users = query.list();
...
session.close();
这个程序片段会使用以下的SQL进行查询:
Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_ from user user0_ for update
也可以在使用Session的load()或是lock()时指定锁定模式以进行锁定。
另外还有三种加锁模式Hibernate内部自动对数据进行锁定,与数据库无关:
LockMode.WRITE:在insert或update时进行锁定,Hibernate会在save()方法时自动获得锁定。
LockMode.READ:在读取记录时Hibernate会自动获得锁定。
LockMode.NONE:没有锁定。
如果数据库不支持所指定的锁定模式,Hibernate会选择一个合适的锁定替换,而不是丢出一个例外
悲观锁定假定任何时刻存取数据时,都可能有另一个客户也正在存取同一条数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其它的客户不能对数据进行存取,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,如果每一次读取数据都造成锁定,其后继的存取就必须等待,这将造成效能上的问题,造成后继使用者的长时间等待。
乐观锁定
乐观锁定(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
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
<hibernate-mapping>
<class name=“com.cstp.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属性,使之与数据库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于数据是由外部系统而来,因而版本信息不受控制时,锁定机制将会有问题,设计时必须注意.