Spring事务管理;hibernate事务管理;jdbc事务管理

Java的JDBC事务详解

事务的特性:

1) 原子性(atomicity):事务是数据库的逻辑工作单位,而且是必须是原子工作单位,对于其数据修改,要么全部执行,要么全部不执行。

2) 一致性(consistency):事务在完成时,必须是所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。

3) 隔离性(isolation):一个事务的执行不能被其他事务所影响。

4) 持久性(durability):一个事务一旦提交,事物的操作便永久性的保存在DB中。即使此时再执行回滚操作也不能撤消所做的更改。

事务(Transaction):是并发控制的单元,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。通过事务,sql server 能将逻辑相关的一组操作绑定在一起,以便服务器 保持数据的完整性。事务通常是以begin transaction开始,以commit或rollback结束。Commint表示提交,即提交事务的所有操作。具体地说就是将事务中所有对数据的更新写回到磁盘上的物理数据库中去,事务正常结束。Rollback表示回滚,即在事务运行的过程中发生了某种故障,事务不能继续进行,系统将事务中对数据库的所有已完成的操作全部撤消,滚回到事务开始的状态。

自动提交事务:每条单独的语句都是一个事务。每个语句后都隐含一个commit。 (默认)

显式事务:以begin transaction显示开始,以commit或rollback结束。

隐式事务:当连接以隐式事务模式进行操作时,sql server数据库引擎实例将在提交或回滚当前事务后自动启动新事务。无须描述事物的开始,只需提交或回滚每个事务。但每个事务仍以commit或rollback显式结束。连接将隐性事务模式设置为打开之后,当数据库引擎实例首次执行下列任何语句时,都会自动启动一个隐式事务:alter table,insert,create,open ,delete,revoke ,drop,select, fetch ,truncate table,grant,update在发出commit或rollback语句之前,该事务将一直保持有效。在第一个事务被提交或回滚之后,下次当连接执行以上任何语句时,数据库引擎实例都将自动启动一个新事务。该实例将不断地生成隐性事务链,直到隐性事务模式关闭为止。

 

Java JDBC事务机制

  首先,我们来看看现有JDBC操作会给我们打来什么重大问题,比如有一个业务:当我们修改一个信息后再去查询这个信息,看是这是一个简单的业务,实现起来也非常容易,但当这个业务放在多线程高并发的平台下,问题自然就出现了,比如当我们执行了一个修改后,在执行查询之前有一个线程也执行了修改语句,这是我们再执行查询,看到的信息就有可能与我们修改的不同,为了解决这一问题,我们必须引入JDBC事务机制,其实代码实现上很简单,一下给出一个原理实现例子供大家参考:

private Connection conn = null;  

private PreparedStatement ps = null;  

 

try {  

    conn.setAutoCommit(false);  //将自动提交设置为false  

              

    ps.executeUpdate("修改SQL"); //执行修改操作  

    ps.executeQuery("查询SQL");  //执行查询操作                 

    conn.commit();      //当两个操作成功后手动提交  

              

} catch (Exception e) {  

    conn.rollback();    //一旦其中一个操作出错都将回滚,使两个操作都不成功  

    e.printStackTrace();  

与事务相关的理论
1.事务(Transaction)的四个属性(ACID)
原子性(Atomic) 对数据的修改要么全部执行,要么全部不执行。
一致性(Consistent) 在事务执行前后,数据状态保持一致性。
隔离性(Isolated) 一个事务的处理不能影响另一个事务的处理。
持续性(Durable) 事务处理结束,其效果在数据库中持久化。

2.事务并发处理可能引起的问题
脏读(dirty read) 一个事务读取了另一个事务尚未提交的数据,
不可重复读(non-repeatable read) 一个事务的操作导致另一个事务前后两次读取到不同的数据
幻读(phantom read) 一个事务的操作导致另一个事务前后两次查询的结果数据量不同。
举例:
事务A、B并发执行时,
当A事务update后,B事务select读取到A尚未提交的数据,此时A事务rollback,则B读到的数据是无效的"脏"数据。
当B事务select读取数据后,A事务update操作更改B事务select到的数据,此时B事务再次读去该数据,发现前后两次的数据不一样。
当B事务select读取数据后,A事务insert或delete了一条满足A事务的select条件的记录,此时B事务再次select,发现查询到前次不存在的记录("幻影"),或者前次的某个记录不见了。

JDBC的事务支持
JDBC对事务的支持体现在三个方面:
1.自动提交模式(Auto-commit mode)
Connection提供了一个auto-commit的属性来指定事务何时结束。
a.当auto-commit为true时,当每个独立SQL操作的执行完毕,事务立即自动提交,也就是说每个SQL操作都是一个事务。
一个独立SQL操作什么时候算执行完毕,JDBC规范是这样规定的:
对数据操作语言(DML,如insert,update,delete)和数据定义语言(如create,drop),语句一执行完就视为执行完毕。
对select语句,当与它关联的ResultSet对象关闭时,视为执行完毕。
对存储过程或其他返回多个结果的语句,当与它关联的所有ResultSet对象全部关闭,所有update count(update,delete等语句操作影响的行数)和output parameter(存储过程的输出参数)都已经获取之后,视为执行完毕。
b. 当auto-commit为false时,每个事务都必须显示调用commit方法进行提交,或者显示调用rollback方法进行回滚。auto-commit默认为true。
JDBC提供了5种不同的事务隔离级别,在Connection中进行了定义。

2.事务隔离级别(Transaction Isolation Levels)
JDBC定义了五种事务隔离级别:
TRANSACTION_NONE JDBC驱动不支持事务
TRANSACTION_READ_UNCOMMITTED 允许脏读、不可重复读和幻读。
TRANSACTION_READ_COMMITTED 禁止脏读,但允许不可重复读和幻读。
TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读,单运行幻读。
TRANSACTION_SERIALIZABLE 禁止脏读、不可重复读和幻读。

3.保存点(SavePoint)
JDBC定义了SavePoint接口,提供在一个更细粒度的事务控制机制。当设置了一个保存点后,可以rollback到该保存点处的状态,而不是rollback整个事务。
Connection接口的setSavepoint和releaseSavepoint方法可以设置和释放保存点。

JDBC规范虽然定义了事务的以上支持行为,但是各个JDBC驱动,数据库厂商对事务的支持程度可能各不相同。如果在程序中任意设置,可能得不到想要的效果。为此,JDBC提供了DatabaseMetaData接口,提供了一系列JDBC特性支持情况的获取方法。比如,通过DatabaseMetaData.supportsTransactionIsolationLevel方法可以判断对事务隔离级别的支持情况,通过DatabaseMetaData.supportsSavepoints方法可以判断对保存点的支持情况。

Hibernate的事务处理机制和flush方法的用法

关于在使用hibernate在提交事务时常遇到的异常:

       an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session)
net.sf.hibernate.AssertionFailure: possible nonthreadsafe access to session

其实这个异常一般都是和我们在操作session flush方法和提交事务过程中会抛出的,下面就具体结合session的事务和声明周期来具体分析下,为什么会有这样的异常;

首先来看下,session的生命周期

Hibernate中java对象的三种状态:

1、临时状态(transient):用new语句创建,还没有被持久化,不处于Session的缓存中。 

2、持久化状态(persistent):已使用save()或者saveOrUpdate()方法,处于Session的缓存中和数据库表中,生成了自己的Oid标识。 

3、游离状态(detached):被持久化,已使用evict(Object),session.close()或者使用clear()清除缓存,不再处于Session的缓存中或不存在数据库表中,但是依然是存在自己的OId标识。 

对象的状态转换

                                                        

从上面的图中我们可以很清楚的明白一个java对象在session中三种状态的转换,

然后在来看看session缓存在什么时候会被清除:

1.当应用程序调用org.hibernate.Transaction的commit()方法的时候,commit()方法先清理缓存,然后再向数据库提交事务。 

2.当应用程序显式调用Session的flush()方法的时候,其实这个方法我们几乎很少用到,因为我们一般都是在完成一个事务才去清理缓存,提交数据更改,这样我们直接提交事务就可以。

缓存清理机制

当Session缓存中对象的属性每次发生了变化,Session并不会立即清理缓存和执行相关的SQL update语句,而是在特定的时间点才清理缓存,这使得Session能够把几条相关的SQL语句合并为一条SQL语句,一遍减少访问数据库的次数,从而提高应用程序的数据访问性能。

在默认情况下,Session会在以下时间点清理缓存。

  1. 当应用程序调用org.hibernate.Transaction的commit()方法的时候.commit方法先清理缓存,然后再向数据库提交事务。Hibernate之所以把清理缓存的时间点安排在事务快结束时,一方面是因为可以减少访问数据库的频率,还有一方面是因为可以尽可能缩短当前事务对数据库中相关资源的锁定时间。
  2. 当应用程序执行一些查询操作时,如果缓存中持久化对象的属性已经发生了变化,就会清理缓存,使得Session缓存与数据库已经进行了同步,从而保证查询结果返回的是正确的数据。
  3. 当应用程序显示调用Session的flush()方法的时候。

Session进行清理缓存的例外情况是,如果对象使用native生成器来生成OID,那么当调用Session的save()方法保存该对象时,会立即执行向数据库插入该实体的insert语句。

image


clear()和evict(Object)的区别:

从参数就可以看出,clear()是会清除整个session中的缓存,evict(Object)是将一个对象从session缓存中清除;

其实在session持久化操作和数据库中之间还有一层对象缓冲区(entityEntries)

 

Commit():此方法在执行后会更新对象在对象缓存区中的existsInDatabase=true;

Flush():会按save,update,delete顺序执行,把缓存中的数据flush入数据库中,并清空缓存区;

下面几个例子可以充分说明我们异常抛出的情况:

SessionFactory sf = new Configuration().configure().buildSessionFactory() ;
Session s = sf.openSession();
Person person = new Person();

Transaction tran = s.beginTransaction(); (1)
s.save(person); (2)(此处同样可以为update delete)
s.evict(person); (3)
tran.commit(); (4)
s.close();(5)

看上面的代码,再参照下我们的示例图和commit()方法,就可以很明显的发现代码问题的所在,在第四步evict()方法将cat对象从对象缓存区清除,当我们执行commit()方法后,更新对象在缓存区中状态的时候,由于已被清除,就会出现上述断言的异常;

Person person1 = new Person ();
person1.setName(“tom”);
s.save(person1);

person1.setName(“mary”);
s.update(person1);

Person person2 = new Person ();
person2.setName(“tom”);
s.save(person2);

s.flush();

其实在这里我们看这个代码的时候感觉是没问题 ,在这里我们可以参考下刚提到的flush()方法,此方法会按save,update,delete的顺序进行提交事务,所以在这里会抛出主键冲突的异常,解决的办法是在update()操作后面也加入flush();

 

总的来说,由于flush()的特殊处理机制,虽然不建议使用此方法,但是在一些复杂的事务处理过程中,加入此方法虽然会破坏事务的一个提交的完整性,但是可以规避一些不可预见的异常情况!

 

hibernate中session接口方法总结

 3980人阅读 评论(0) 收藏 举报


Session的save()和persist()方法
Session的save()方法使一个临时对象转变为持久化对象。它完成以下操作:
(1)将临时对象加入到Session缓存中,使其进入持久化状态。
(2)选用映射文件指定的标识符生成器,为持久化对象分配唯一的OID。
(3)计划执行一个insert语句。  
Session的save()方法是用来持久化临时对象的。不应将持久化对象或游离对象传递给save()方法。
若将持久化对象传递给save()方法,则该步保存操作是多余的。
若将游离对象传递给save()方法,则会重新生成OID,再保存一次。

Session的persist()方法与save()方法类似,也能将临时对象转变为持久化对象。
persist()方法与save()方法的区别在于:
persist()方法不保证立即为持久化对象的OID赋值,而是有可能在Session清理缓存的时候才为OID赋值。
此外,如果是在事物边界以外调用persist()方法,则该方法不会计划执行insert语句。而save()方法不论是在事物边界以外还是以内,都会计划执行insert语句。

Session的load()与get()方法
Session的load()与get()方法都能从根据给定的OID从数据库中加载一个持久化对象,这两个方法的区别在于:
(1)当数据库中不存在与OID对应的记录时,load()方法抛出org.hibernate.ObjectNotFoundException异常,而get()方法返回为null。
(2)load方法采用配置的加载策略(默认为延迟加载),而get()方法则会忽略配置,总是采用立即加载方法。

Session的update()方法
Session的update()方法使一个游离对象转变为持久化对象。它完成以下操作:
(1)将游离对象加入到Session缓存中,使其转变为持久化对象。
(2)计划执行一个update语句。
当update()方法关联一个游离对象时,若在Session的缓存中已存在相同的OID的对象时,会抛出异常。

疑问:如果数据库总没有对应的记录,会产生异常? update语句如果没有记录更新是不会报错的呀?


Session的saveOrUpdate()方法
Session的saveOrUpdate()方法同时包含了save()方法与update()方法的功能,如果传入的参数是临时对象,就调用save()方法;如果传入的参数是游离对象,就调用update()方法。HIbernate根据对象的OID,version版本属性等来判断参数是临时对象还是游离对象。

Session的merge()方法
Session的merge()方法能够将一个游离对象的属性复制到一个持久化对象中。其处理流程如下:
(1)根据游离对象的OID到Session缓存中查找匹配的持久化对象。若找到匹配的持久化对象,则将游离对象的属性复制到持久化对象中,计划实行一条update语句,然后返回持久化对象的引用。
(2)如果在Session的缓存中没有找到与游离对象OID一致的持久化对象,那么就试图根据该OID从数据库中加载持久化对象。如果数据库中存在匹配的持久化对象,则将游离对象的属性复制到刚加载的持久化对象中,计划实行一条update语句,然后返回持久化对象的引用。
(3)如果数据库中也不存在or对象是临时对象时,则会新建一个对象,将属性赋值到该新建对象中,再持久化新建对象,最后返回新建对象的引用。

merger()和saveOrUpdate()的区别:

调用完meger()对象仍然是脱管状态。


Session的delete()方法
Session的delete()方法用于从数据库中删除一个Java对象。delete()方法既可以删除持久化对象,也可以删除游离对象。其处理过程如下:
(1)如果传入的参数是游离对象,则先使游离对象与Session关联,使它变为持久化对象。如果参数是持久化对象,则忽略该步。
(2)计划执行一个delete语句。
(3)把对象从Session缓存中删除,该对象进入删除状态。

Session的replicate()方法
Session的replicate()方法能够将一个数据库中的对象复制到另一个数据库中。


Spring事务处理你未关注过的原理
本文对Spring实现事务处理的真正原理进行追究,从而从中提炼出一些见解。其中讲解内容可能会存在一定的误导,还希望指出,内容仅供参考!(经过本人后期继续研读Spring关于Mybatis的事务处理,其实在mybatis的里面调用了spring的方法来获取Connection,所以本文所提供的一种实现,是另一种Spring的实现猜想,仅供参考!)
  说到Spring事务原理,百度一下最多的就是Spring的AOP了,本文当然不是给你将AOP的原理,如果是这样,我也就没必要写这篇文章了,直接转载一篇就行了。借助Spring的AOP的原理,提出一个问题。
            此处先粘贴出Spring事务需要的配置内容:
[html] view plain copy
  1.        <bean id="transactionManager"  
  2. class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  3. <property name="dataSource" ref="dataSource" />.....  
  4. lt;/bean>  

 
[html] view plain copy
  1.         <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
  2. <property name="dataSource" ref="dataSource" />  
  3. .....         
  4. lt;/bean>  
            上面两段配置文件一个是Spring事务管理器的配置文件,另一个是一个普通的JPA框架(此处是mybatis)的配置文件,这两个里面都配置了datasource,而且这个datasource的对象是在Spring的容器里面。一下提几个问题:
            1、当JPA框架对数据库进行操作的时候,是从那里获取Connection?
            2、jdbc对事务的配置,比如事务的开启,提交以及回滚是在哪里设置的?
            3、Spring是通过aop拦截切面的所有需要进行事务管理的业务处理方法,那如何获取业务处理方法里面对数据库操作的事务呢?
           现在我来对上面的问题来一一回答
           1、这个问题很简单,既然在JPA的框架里面配置了datasource,那自然会从这个datasource里面去获得连接。
           2、jdbc的事务配置是在Connection对消里面有对应的方法,比如setAutoCommit,commit,rollback这些方法就是对事务的操作。
           3、Spring需要操作事务,那必须要对Connection来进行设置。Spring的AOP可以拦截业务处理方法,并且也知道业务处理方法里面的DAO操作的JAP框架是从datasource里面获取Connection对象,那么Spring需要对当前拦截的业务处理方法进行事务控制,那必然需要得到他内部的Connection对象。整体的结构图如下:
           
           上图是一个标准的业务处理在一个线程上的基本流程。JPA框架在需要对数据库进行操作的时候,就会从Datasource里面去获取Connection对象,那么Spring是怎么样拿到在JPA内部调用的Connection并且加上用户配置的事务处理规则的呢?现在我来揭开这个谜底。
           上面也看到注入到Spring的事务管理的datasource和注入到第三方JPA框架的datasource都是在Spring容器里面的,并且是同一个对象。既然Spring可以拿到你这个datasource对象,那它为什么不进行一下封装呢?不管是哪家的Datasource他们都会实现javax.sql.DataSource这个接口,这个接口里面主要有两个方法
[java] view plain copy
  1. <span style="font-size:18px">public interface DataSource  extends CommonDataSource,Wrapper {  
  2.   
  3.   /** 
  4.    * <p>Attempts to establish a connection with the data source that 
  5.    * this <code>DataSource</code> object represents. 
  6.    * 
  7.    * @return  a connection to the data source 
  8.    * @exception SQLException if a database access error occurs 
  9.    */  
  10.   Connection getConnection() throws SQLException;  
  11.         
  12.   /** 
  13.    * <p>Attempts to establish a connection with the data source that 
  14.    * this <code>DataSource</code> object represents. 
  15.    * 
  16.    * @param username the database user on whose behalf the connection is  
  17.    *  being made 
  18.    * @param password the user's password 
  19.    * @return  a connection to the data source 
  20.    * @exception SQLException if a database access error occurs 
  21.    * @since 1.4 
  22.    */  
  23.   Connection getConnection(String username, String password)   
  24.     throws SQLException;</span><span style="font-size:18px">  
  25. </span>  

              这两个方法均是获取Connection对象的。Spring有没有可能对这个接口创建一个代理呢?通过spring的AOP。然后偷偷将Spring容器里面的datasource的bean指向这个代理对象(此处称该对象为datasourceproxy,替换之前的叫datasource)。于是不管是从哪里调用Datasource,那必然会被Spring拦截。下面是模拟了一个简单实现:
[java] view plain copy
  1. public class DatasourceHandler implements InvocationHandler {  
  2.   
  3.     private DataSource dataSource;  
  4.     /** 
  5.      * @param dataSource 
  6.      */  
  7.     public DatasourceHandler(DataSource dataSource) {  
  8.         super();  
  9.         this.dataSource = dataSource;  
  10.     }  
  11.     /* (non-Javadoc) 
  12.      * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) 
  13.      */  
  14.     @Override  
  15.     public Object invoke(Object proxy, Method method, Object[] args)  
  16.             throws Throwable {  
  17.         if(method.getName().equals("getConnection")){  
  18.               
  19.             if(ResourceHolder.getResource(proxy)!=null){  
  20.                 Connection connection = (Connection) method.invoke(this.dataSource, args);  
  21.                 ResourceHolder.addResource(proxy, connection);  
  22.             }  
  23.             return ResourceHolder.getResource(proxy);  
  24.         }else{  
  25.             return method.invoke(this.dataSource, args);  
  26.         }  
  27.     }  
  28. }  


[java] view plain copy
  1.   
[java] view plain copy
  1.   

         上面这个类是一个InvocationHandler的实现,假设这个就是Spring Aop拦截Datasource的实现。那么这个对象里面有一个datasource对象,这个对象是Spring替换代理Datasource之前的那个对象(datasource)。看到invoke的实现,其实就是将代理(datasourceproxy)调用的类发转到datasource去调用,其实还是调用了datasource,但是这里就加入了一些特殊的东西,那就是ResourceHolder
[java] view plain copy
  1. <span style="font-size:18px">public class ResourceHolder {  
  2.   
  3.     private static ThreadLocal<Map<Object,Object>> resources= new ThreadLocal<Map<Object,Object>>();  
  4.       
  5.     public static void addResource(Object key,Object value){  
  6.         if(resources.get()==null){  
  7.             resources.set(new HashMap<Object,Object>());  
  8.         }  
  9.         resources.get().put(key, value);  
  10.     }  
  11.       
  12.     public static Object getResource(Object key){  
  13.           
  14.         return resources.get().get(key);  
  15.     }  
  16.       
  17.     public static void clear(){  
  18.         resources.remove();  
  19.     }  
  20. }</span>  
 上面是这个对象的实现,里面有一个ThreadLocal静态属性,用于存放一些数据信息。ThreadLocal用过的人都知道他是线程的局部变量,在整个线程过程中都是有效的。那么在invoke里面当每次调用的时候,判断调用的方法是不是getConnection,如果是,则进行如下操作
if(ResourceHolder.getResource(proxy)!=null){
Connection connection =(Connection) method.invoke(this.dataSource, args);
ResourceHolder.addResource(proxy, connection);
}
return ResourceHolder.getResource(proxy);
其中proxy就是Spring自动生成的datasourceproxy,将proxy和connection的关系添加到ResourceHolder里面去,而ResourceHolder又是将这个关系添加到ThreadLocal<Map<Object,Object>> resources这个静态变量里面,添加到这个里面,那么以后如果在当前线程从datasourceproxy获取connection对象,都将是一个对象,这就保证了一个业务方法里面进行多次dao操作,调用的都是一个connection对象,同时保证了多个dao都是在一个事务里面。既然这样,那么Spring的事务管理就可以在调用业务方法之前,先从datasource里面先获得一个connection对象,并且对connection添加上用户配置的事务规则,由于这个connection对象会自动添加到ThreadLocal里面,那么后面的业务处理方法将会是调用已经添加好事务规则的connection对象,当业务方法处理完毕,那么spring事务就可以对这个connection进行回滚或者提交了。经过这样一个过程,那么在一个处理某个业务的线程里面执行流程应该是这样的:

               
        总结一下:这里首选是对DataSource生成一个代理类,从而可以监控获取Connection的过程,在通过ThreadLocal对Connection线程级别的缓存从而促使在同一个业务处理方法相对于某个DataSource都是在一个Connection中,从而保证处于同一事务中,因为这些执行都是在一个线程中的。这里处理Spring的AOP之外,还有一个ThreadLocal的使用。在实践编程中,有时候你会发现ThreadLocal会带来很大的帮助。
        比如,你要在某个操作中的每个处理流程都要知道操作人信息,而且这个流程可能不是在一个方法或者一个类中处理完,如果在session环境中,你可能会考虑用session,但不是所有的开发都是在Session环境中的,那么此时ThreadLocal边是最好的帮手,可以在用户触发这个操作时候将用户信息放在ThreadLocal中,那么后面的每个流程都可以从ThreadLocal中获取,而且这个是线程范围的,每个线程中的ThreadLocal是不相干的,这样也防止了多线程的操作。

展开阅读全文

没有更多推荐了,返回首页