Hibernate 学习笔记(6)

Hibernate中的事务管理以及缓存管理

事务的概念(前面已经讲述) 

Hibernate中的Session缓存(前面已经讲述) 

Hibernate中的二级缓存以及二级缓存的缓存策略

Query查询缓存

Query.list ()、iterator()深入

悲观锁定(Pessimistic Locking)

乐观锁定(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属性,使之与数据库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于数据是由外部系统而来,因而版本信息不受控制时,锁定机制将会有问题,设计时必须注意.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值