Hibernate事务

我们知道Hibernate是对JDBC的轻量级对象封装,它本身不具备事务处理能力,只是对底层JDBC事务或JTA事务进行了封装。具体使用什么事务可以在配置文件中进行配置:

    <session-factory>

        <property name="hibernate.transaction.factory_class">

            org.hibernate.transaction.JTATransactionFactory

            <!--org.hibernate.transaction.JDBCTransactionFactory-->

        </property>

    </session-factory>

默认情况下使用的是JDBCTransaction。在实际应用中,我们实际是将事务管理委托给Spring来进行管理的。

我们知道事务是为了保证用户操作的原子性 ( Atomicity )、一致性 ( Consistency )、隔离性 ( Isolation ) 和持久性 ( Durabilily )。所以我们从Hibernate的并发、事务隔离和长事务几个方面来总结一下Hibernate的事务管理。

 

在Hibernate中,SessionFactory对象被设计成线程安全的对象,可以为所有线程所共享,而session对象则是一个轻型的非线程安全的对象。通常我们在请求中通过SessionFactory创建一个session,使用后关闭该Session,如:

    //创建Configeration类的实例,将配置信息(Hibernate config.xml)读入到内存。

    Configuration config = new Configuration().configure();

    //创建SessionFactory实例,SessionFactory的实例代表一个数据库存储员源,创建后不再与Configeration 对象关联。

    SessionFactory sf = conf.buildSessionFactory(); 

    //调用SessionFactory创建Session的方法

    Session session = sf.openSession(); 

    //通过Session 接口提供的各种方法来操纵数据库访问。

    try{ 

      Transaction tx = session.beginTransaction(); 

      User user = new User(); 

      user.setName("wuyuan"); 

      user.setBirthday(new Date()); 

      session.save(user);  

      tx.commit(); 

    }catch(Exception e){ 

      e.printStackTrace(); 

    } finally{ 

      session.close(); 

      session = null; 

    }

 

通常我们不允许在多线程中共享Session。然而在一个请求中使用Session时,我们同时需要考虑事务的持续。Session通过在需要的时候会获取一个数据库(资源)连接,并对应了一个数据库事务,如果事务持续时间长,那么占用的数据库资源就长,数据库并发处理的能力就会降低。所以数据库事务应尽可能的短,降低数据库锁定造成的资源占用。在应用中仔细确定事务的边界,必要时可以使用拦截器来确定Session和事务被正确的开启和结束。我们在SSH中通常将事务边界确定在service层,一般来讲是没有问题的。但我们在service中还可以分离一些逻辑将事务边界尽可能短,从而使一些无需包括在事务内的代码排除在事务外。此外,在一些大数据量的采集接口中,我个人觉得需要将事务边界定得更加短,将事务边界确定在Dao层更好,特别是在这些接口同时可能在频率调用时,这样可以将一些计算放在事务边界外处理,并将处理结果传入事务中。当然如果是高性能高并发的应用中不建议使用Hibernate框架。

 

多个事务并发访问同一资源时,可能会引发更新丢失、脏读、幻读、不可重复读等问题。解决方案是设置事务隔离级别。通常在一般的应用中,常采用读取提交内容(read committed isolation)、可重读(repeatable read isolation)两种。

如:MySQL的默认事务隔离级别Repeatable Read(可重读),它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,即允许幻读,但不允许不可重复读和脏读。ORACLE默认隔离级别是read committed,它允许幻读和不可重复读,但不允许脏读。

 

如果在数据库中采用Read Committed的隔离级别,我们可以在程序中进行处理实现Repeatable Read 级的隔离级别。Hibernate提供了乐观锁机制来帮助进行控制。乐观锁的实现方式有:version number、timestamp。

Hibernate参考文档中提到” 唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发控制。版本检查使用版本号、或者时间戳来检测更新冲突(并且防止更新丢失)”。

所谓版本号就是通过为数据增加一个版本标志version,通过比较version来判断数据是否发生变化。在读取数据时一同读出该数据的版本,更新数据时对数据的版本加1,在提交数据时与数据库中的相应数据进行比较,若版本号大于数据库中的版本号则认为是新数据,更新数据库中的数据;如果小于数据库中的号则认为是过期数据。

在应用程序中实现版本检查,需要为对象增加一个version属性来表示记录的版本(version 属性使用<version>来映射,如下:

public class WorkEffort {

  private String id;

  private String name;

  private int version;

 

  public int getVersion() {

    return version;

  }

  public void setVersion(int version) {

    this.version = version;

  }

}

WorkEffort.hbm.xml

<class name="org.sample.WorkEffort" table="workeffort" >

    <id name="id" column="id">

      <generator class="assigned"/>

    </id>

    <property name="name" type="string" column="name"></property> 

    <version name="version" column="ver" type="int"></version>

</class>

应用代码如下:

Session session = factory.openSession();

Transaction tx = session.beginTransaction();

int oldVersion = workEffort.getVersion();

session.load( workEffort, workEffort.getId() ); // load the current state

if ( oldVersion!=workEffort.getVersion ) throw new StateException();

workEffort.setName("new Name");

tx.commit();

session.close();

此时,Hibernate同步的时候会自动增加版本号(注意:如果属性optimistic-lock被设置为false,则Hibernate禁止版本的自动增加)。

如果映射中设置 optimistic-lock=”all”,则可以在没有版本或者时间戳属性映射的情况下实现 版本检查,此时Hibernate将比较一行记录的每个字段的状态。

如果映射中设置optimistic-lock="dirty",Hibernate在同步的时候将只比较有脏 数据的字段。

 

在应用程序中实现时间戳检查,需要为对象增加一个Timestamp属性来表示记录更新时间,如下:

public class WorkEffort {

  private String id;

  private String name;

  private Timestamp createdDate;

  private Timestamp lastModifiedDate;

 

  public Timestamp getLastModifiedDate() {

    return lastModifiedDate;

  }

 

  public void setLastModifiedDate(Timestamp lastModifiedDate) {

    this.lastModifiedDate = lastModifiedDate;

  }

}

WorkEffort.hbm.xml

<class name="org.sample.WorkEffort" table="workeffort" >

    <id name="id" column="id">

      <generator class="assigned"/>

    </id>

<property name="name" type="string" column="name"></property> 

    <property generated="never" name="lastModifiedDate" type="java.sql.Timestamp" update="true">

        <column name="LAST_MODIFIED_DATE" sql-type="TIMESTAMP"/>

    </property>

    <timestamp name="lastModifiedDate" column="LAST_MODIFIED_DATE"/>

    <!--以下可利用数据库机制来实现时间戳-->

    <!-- property generated="never" name="createdDate" type="java.sql.Timestamp" update="false">

        <column name="CREATED_DATE"/>

    </property -->

</class>

在应用中就可以进行时间戳比较来判断是否存在更新。

使用注解方式如下:

    @Column(name="CREATED_DATE", updatable=false)  

    @Temporal(TemporalType.TIMESTAMP)  

    @Generated(GenerationTime.INSERT)  

    private Timestamp createdDate;  

      

    @Column(name="LAST_MODIFIED_DATE")  

    @Temporal(TemporalType.TIMESTAMP)  

    @Generated(GenerationTime.NEVER)  

    private Timestamp lastModifiedDate; 

 

虽然我们希望将事务范围定义的足够短,但在应用中还可能出现一些长事务而进行大批量数据处理的情况,这些事务往往占据大量系统资源,并且运行时间也比较长。数据库长事务也会导致应用程序无法扩展到高的并发负载。因此,最晚提交生效(last commit wins)是应用程序长事务的默认处理策略。

针对长事务,我们可以将数据对象始终绑定到加载的Session中,通过控制Session来实现最晚提交生效策略。通过Session.reconnect()获取一个新的数据库连接,并且继续当前的session。用Session.disconnect()方法把session与JDBC连接断开,把数据库连接返回到连接池。在Session重新连接上数据库连接之后,可以对任何可能被其他事务更新过的对象调用Session.lock(),设置LockMode.READ锁定模式,这样就可以对那些不准备更新的数据进行强制版本检查。此外,并不需要锁定那些准备更新的数据。假若对disconnect()和reconnect()的显式调用发生得太频繁了,还可以使用hibernate.connection.release_mode来代替。如:

// foo is an instance loaded earlier by the Session

session.reconnect(); // Obtain a new JDBC connection

Transaction t = session.beginTransaction();

foo.setProperty("bar");

t.commit(); // End database transaction, flushing the change and checking the version

session.disconnect(); // Return JDBC connection

 

Session的lock()方法与update()方法的区别

lock()方法:

1.在执行lock()方法时,如果设定了LockMode.READ模式,则立即进行版本检查,使用类似以下形式的查询语句:select ID from table where ID = 1 and VERSION = 0;如果数据库中没有匹配的记录,就抛出StaleObjectStatException。更改持久状态的对象的内容

2.并不会计划执行一个update语句。

 

update()方法:

1.在执行update()方法时,并不会进行版本检查,直到Session清理缓存时才会进行版本检查,如果数据库中没有匹配的记录,就抛出StaleObjectStatException。

2.会计划执行一个update语句。

Session的lock()方法与update()方法的相同点

把游离对象与当前session关联(把对象从脱管状态变成持久状态)

 

我们也可以使用悲观锁解决repeatable read 的问题。所谓悲观锁是假定当前事务在操纵数据资源时,肯定还会有其它事务同时在访问该数据资源,为了避免当前事务的操作受到干扰而首先锁定资源。所以悲观锁虽然能防止丢失更新和不可重复读等并发问题,但会影响并发性能。所以一般不推荐使用悲观锁(另一方面,由于悲观锁实现是使用数据库的锁定机制,使用悲观锁需要了解所连接的数据库的锁机制)。

Hibernate总是使用数据库的锁定机制,从不在内存中锁定对象。我们可以在配置中设置hibernate中的事务隔离级别(hibernate.connection.isolation),如果不设置,则默认依赖数据库本身的级别。(如果数据库不支持设置的锁定模式,Hibernate 将使用适当的替代模式)

在应用有几种方式可以显示指定锁定模式

1)session的get()或load()

2)session的lock()

3)Query的setLockMode()

如:

Transaction ts = session.beginTransaction();

Query query = session.createQuery("from Table as t");

query.setLockMode("t", LockMode.UPGRADE); //此处t是表持久化类的别名,对其返回的所有对象加锁

List list = query.list();

ts.commit();

只有在调用Query的list()方法之前加锁,才能通过数据库的锁机制执行加锁处理,否则无法加锁。

再如:

Transaction tx = session.beginTransaction();  

Order order = (Order)session.load(Order.class, 1,LockMode.UPGRADE);

order.setTotAmount(order.getTotAmount () - 150);  

tx.commit();  

session.close(); 

 

类LockMode定义了Hibernate所需的不同的锁定级别。一个锁定可以通过以下的机制来设置:

◆当Hibernate更新或者插入一行记录的时候,锁定级别 自动 设置为LockMode.WRITE。

◆当用户显式的使用数据库支持的SQL格式SELECT...FOR UPDATE发送SQL的时候,锁定级别设置为LockMode.UPGRADE。

◆当用户显式的使用Oracle数据库的SQL语句SELECT...FOR UPDATE NOWAIT的时候,锁定级别设置LockMode.UPGRADE_NOWAIT。

◆当Hibernate在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式自动设置为LockMode.READ。这种模式也可以通过用户显式指定进行设置。

◆LockMode.NONE代表无需锁定。在Transaction结束时,所有的对象都切换到该模式上来。与session相关联的对象通过调用update()或者saveOrUpdate()脱离该模式。

 

“显式的用户指定”可以通过以下几种方式之一来表示:

◆调用Session.load()的时候指定锁定模式(LockMode)。

◆调用Session.lock()。

◆调用Query.setLockMode()。

 

附:LockMode类表示的几种锁定模式

锁定模式

描述

LockMode.NONE

如果缓存中存在对象,直接返回该对象的引用,否则通过select语句到数据库中加载该对象,默认值.

LockMode.READ

不管缓存中是否存在对象,总是通过select语句到数据库中加载该对象,如果映射文件中设置了版本元素,就执行版本检查,比较缓存中的对象是否和数据库中对象版本一致

LockMode.UPGRADE

不管缓存中是否存在对象,总是通过select语句到数据库中加载该对象,如果映射文件中设置了版本元素,就执行版本检查,比较缓存中的对象是否和数据库中对象的版本一致,如果数据库系统支持悲观锁(如Oracle/MySQL),就执行select...for update语句,如果不支持(如Sybase),执行普通select语句

LockMode.UPGRADE_NOWAIT

和LockMode.UPGRADE具有同样功能,此外,对于Oracle等支持update nowait的数据库,执行select...for update nowait语句,nowait表明如果执行该select语句的事务不能立即获得悲观锁,那么不会等待其它事务释放锁,而是立刻抛出锁定异常

LockMode.WRITE

保存对象时会自动使用这种锁定模式,仅供Hibernate内部使用,应用程序中不应该使用它

LockMode.FORCE

强制更新数据库中对象的版本属性,从而表明当前事务已经更新了这个对象

 

参考:高效使用JavaEE ORM 框架

             Hibernate参考文档

posted @ 2013-04-16 23:20  Jevo  阅读(337)  评论(0)    收藏  举报