一、开篇陈述
1.1 写文缘由
最近在系统学习spring框架IoC、AOP、Transaction相关的知识点,准备写三篇随笔记录学习过程中的感悟。这是第一篇,记录spring Transaction的使用及部分原理。spring的学习过程应该是从IoC到AOP再到Transaction,这里写随笔的顺序没有按照学习路线。
1.2 预备技能
学习spring事务使用最好具备基础的Java知识,掌握spring IoC和spring AOP,并对各类数据库事务的概念有所了解。当然,这些都不是必须的,如果你相信你的理解力。笔者在学习spring 事务之前有很多疑问:如为什么数据库在被事务中的操作改变之后,事务还可以进行回滚,数据库可以像没有操作过一样?事务进行过程中,其它读写数据库的操作看到怎样的结果,会不会看到事务中非完整的操作结果?带着这些问题,开始学习spring事务的使用吧。
二、基本概念和主要接口
2.1 基本概念
为什么需要事务:应用中需要保证用户的操作的可靠性和完整性,有些操作必须作为一组原子操作(如转账、下单减库存等)提交到数据库,如果其中的一个操作失败,其它操作也不应该生效,这就是数据库事务的概念(下单过程中减了库存,随后的下单记录添加失败,那么库存就不应该减,所以应该将这个步骤作为一个事务提交到数据库,以保证数据完整性)。
事务的一些属性:事务有一些属性来描述,其中最重要的有事务的隔离级别、事务的传播属性、事务的超时时间和事务的只读属性。
隔离级别:隔离级别是指多个事务同时执行时,各个事务之间的影响相互隔离的程度。主要有如下几个级别:
ISOLATION_DEFAULT(底层数据库默认隔离级别,通常为ISOLATION_READ_COMMITTED,这个级别的事务只能看到其它事务已经提交的修改,没有提交的修改都不能被看到,如减库存操作操作完成,下单还没完成,整个事务没有提交,那么这种隔离级别的其它事务是没法看到减库存成功的操作结果的,只有整个事务提交之后才能看到,这也是我们常用的默认隔离级别)
ISOLATION_READ_UNCOMMITTED(可以读取另一个事务修改但还没有提交的数据,会导致脏读和不可重复读,很少使用;比如减库存操作完成,其它事务就能看到库存被减了,如果这时候库存正好被减为0,其它用户可能就下单失败,但是如果这个事务最后失败了,库存被回滚,又有可能被其它用户购买)
ISOLATION_READ_COMMITTED(只能读取已提交的数据,可以防止脏读,还是存在不可重复读)
ISOLATION_REPEATABLE_READ(可以多次重复执行某个查询,并且每次返回的记录都相同。有新增数据满足查询也会被忽略,防止脏读和不可重复读。当库存减少时,已经在执行的其它事务看不到这个减少吗?这个太奇怪了。暂时还没搞清楚实现原理)
ISOLATION_SERIALIZABLE(事务依次逐个执行)
读一致性:上面的隔离级别中我们关心的都是读数据的返回,因为写肯定是要互斥且顺序执行的,写不存在并行。下面研究一下读数据的一致性。
脏读(一个事务访问并修改了数据,修改还没提交,另一个事务也访问并使用这个数据;库存被减了,但是还没查下单记录,事务未提交,另一个事务读库存,发现库存为0,于是下单失败,这个时候如果事务回滚,其实库存还是有的,读到了脏数据)
不可重复读(一个事务内多次读同一数据,在这之间,另一个事务修改了数据,导致一个事务内两次读到的数据是不一样的;两次读库存,中间库存被修改)
幻读(事务不是独立执行,在第一个事务对表数据进行修改后,第二个事务也修改了表数据,然后第一个事务发现表中的数据跟预想的不一致;如第一个事务减库存失败,第二个事务增加了库存,第一个事务发现莫名其妙的库存变化,要防止幻读只能用串行执行的隔离级别)
传播属性:当事务开始时,一个事务上下文已经存在,此时可以指定一个事务性方法的执行行为。
PROPAGATION_REQUIRED(有则加入,无则新建)
PROPAGATION_REQUIRES_NEW(新建事务,挂起之前的事务)
PROPAGATION_SUPPORTS(有则加入,没有则以非事务方式运行)
PROPAGATION_NOT_SUPPORTED(有则挂起当前事务)
PROPAGATION_NEVER(有则抛异常)
PROPAGATION_MANDATORY(有则加入,没有则抛异常)
PROPAGATION_NESTED(有则以嵌套事务的方式执行,外部事务提交才会触发内部事务提交,外部事务回滚会触发内部事务回滚)
2.2 主要接口
事务最主要的API:
TransactionDefinition(事务规则:设置事务的一些属性,上文提到的,使用DefaultTransactionDefinition默认实现一般可以满足要求,或者可以扩展接口,实现自己的定义)
PlatformTransactionManager(事务管理:spring没有直接管理事务,而是将事务管理的责任委托给JTA或持久化机制的某个特定平台的事务实现。spring的事务管理器充当了特定平台事务的代理,如下图所示)
TransactionStatus(事务状态:代表一个新的或已经存在的事务,控制事务执行和查询事务状态)
三、编程式事务和声明式事务
所谓编程式事务就是在业务代码中显示编写事务逻辑,而声明式事务则是在配置文件中声明事务,基本不在代码中影响bean的工作方式。
3.1 编程式事务
1)基于底层API的编程式事务管理
这种方式直接使用PlatformTransactionManager、TransactionDefinition、TransactionStatus三个核心接口编程实现事务。示例代码如清单1、2所示:
清单1:业务逻辑
1 @Service("testDao") 2 3 public class TestDaoImpl implements TestDao { 4 5 6 7 @Resource(name="dataSource") 8 9 private DataSource dataSource; 10 11 12 13 @Resource(name="txDefinition") 14 15 private TransactionDefinition txDefinition; 16 17 18 19 @Resource(name="txManager") 20 21 private PlatformTransactionManager txManager; 22 23 24 25 public void insert(String key, Object value) { 26 27 TransactionStatus txStatus = txManager.getTransaction(txDefinition); 28 29 System.out.println("trans status: " + txStatus.isNewTransaction() + txStatus.isRollbackOnly() + txStatus.isCompleted()); 30 31 try { 32 33 JdbcTemplate jt = new JdbcTemplate(dataSource); 34 35 int ret1 = jt.update("insert into kv (k, v)" 36 37 + " values('" + key + "', '" + value + "')"); 38 39 System.out.println("insert first time. ret1 = " + ret1); 40 41 int ret2 = jt.update("insert into kv (k, v)" 42 43 + " values('" + key + "', '" + value + "')"); 44 45 System.out.println("insert second time.ret2 = " + ret2); 46 47 48 49 txManager.commit(txStatus); 50 51 System.out.println("is completed: " + txStatus.isCompleted()); 52 53 } catch (Exception e) { 54 55 txManager.rollback(txStatus); 56 57 System.out.println("is rollback: " + txStatus.isRollbackOnly()); 58 59 System.out.println("caught exception: --------"); 60 61 e.printStackTrace(); 62 63 64 65 } 66 67 } 68 69 }
清单2:主程序
1 public class App 2 3 { 4 5 private static ApplicationContext context = new ClassPathXmlApplicationContext("spring/spring.xml"); 6 7 8 9 public static void main( String[] args ) 10 11 { 12 13 TestDao testDao = (TestDao)context.getBean("testDao"); 14 15 try { 16 17 testDao.insert("i am JUNE", "June is excellent!"); 18 19 } catch (Exception e) { 20 21 System.out.println("out side caughter -----"); 22 23 e.printStackTrace(); 24 25 26 27 } 28 29 System.out.println( "Hello World!" ); 30 31 } 32 33 }
清单3:配置文件
1 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 2 <property name="driverClassName" value="com.mysql.jdbc.Driver" /> 3 <property name="url" value="jdbc:mysql://10.13.49.201:3306/database" /> 4 <property name="username" value="dmp" /> 5 <property name="password" value="test" /> 6 </bean> 7 <bean id="txDefinition" class="org.springframework.transaction.support.DefaultTransactionDefinition"> 8 <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property> 9 <property name="timeout" value="10000"></property> 10 </bean> 11 <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 12 <property name="dataSource" ref="dataSource" /> 13 </bean>
如上清单1所示,示例中配置了三个bean(数据源dataSource、事务定义txDefinition以及事务管理器txManager),编程式事务开始于txManager.getTransaction,终止于txManager.commit(事务过程中没有异常,事务被提交)或txManager.rollback(事务过程中抛出异常,事务被回滚)。示例中用于测试的事务向数据库kv表中插入两条相同的数据,由于kv表中对key字段做了唯一性约束,所以在插入第二条数据的时候会抛出异常,如果没有事务保证,数据库会被插入一条数据,而第二条数据不能插入,示例将两次插入放入事务中,当第二次插入时会抛出异常,事务被回滚,第一条数据也不会真正插入到数据库。编写这类事务需要注意在合适的地方进行事务提交或回滚。
2)基于TransactionTemplate的编程式事务管理
从上面的例子可以看出,那种方式的事务管理存在很多样板代码,如事务开始、捕获异常、事务提交和事务回滚,这些代码严重破坏了业务代码的结构,spring提供一个改进的方式编程实现事务。示例代码如清单4所示:
清单4:基于TransactionTemplate实现的事务逻辑
1 @Service("testDaoTemplate") 2 3 public class TestDaoTemplateImpl implements TestDao{ 4 5 6 7 @Resource 8 9 private DataSource dataSource; 10 11 12 13 @Resource 14 15 private TransactionTemplate txTemplate; 16 17 18 19 public void insert(final String key, final Object value) { 20 21 txTemplate.execute(new TransactionCallback() { 22 23 24 25 public Object doInTransaction(TransactionStatus status) { 26 27 System.out.println("trans status: " 28 29 + status.isNewTransaction() 30 31 + status.isRollbackOnly() 32 33 + status.isCompleted()); 34 35 try { 36 37 JdbcTemplate jt = new JdbcTemplate(dataSource); 38 39 int ret1 = jt.update("insert into kv (k, v)" 40 41 + " values('" + key + "', '" + value + "')"); 42 43 System.out.println("insert first time. ret1 = " + ret1); 44 45 int ret2 = jt.update("insert into kv (k, v)" 46 47 + " values('" + key + "', '" + value + "')"); 48 49 System.out.println("insert second time.ret2 = " + ret2); 50 51 52 53 System.out.println("is completed: " + status.isCompleted()); 54 55 } catch (Exception e) { 56 57 status.setRollbackOnly(); 58 59 System.out.println("is rollback: " + status.isRollbackOnly()); 60 61 62 63 System.out.println("caught exception: --------"); 64 65 e.printStackTrace(); 66 67 68 69 } 70 71 72 73 return null; 74 75 } 76 77 78 79 }); 80 81 } 82 83 }
清单5:配置文件
1 <bean id="txTemplate" class="org.springframework.transaction.support.TransactionTemplate"> 2 3 <property name="transactionManager" ref="txManager"></property> 4 5 </bean>
这种方式只是将这些模板代码封装到TransactionTemplate中,其业务逻辑写在一个TransactionCallback内部类中,作为txTemplate的参数传递进去。默认规则是执行回调方法的过程中抛出unchecked异常或显示调用setRollbackOnly方法,事务将被回滚,否则(未抛异常或抛出异常被捕获而没有显示调用setRollbackOnly)提交事务。这类方式比底层API的方式稍微简便些,但是仍然破坏了业务代码的结构。
3.2 声明式事务
声明式事务建立在AOP(事务管理本身就是一个典型的横切逻辑)的基础之上,本质是对方法进行拦截,方法开始前加入事务,方法执行完后根据情况提交或回滚事务。声明式事务最大的优点就是不需要在业务逻辑中掺杂事务管理的代码,只在配置文件中做相关的事务规则声明(大型项目中严重建议使用声明式事务)。声明式事务的缺点就是事务的最细粒度只能作用到方法级别。
1) 基于TransactionInterceptor类实现的声明式事务
清单6: 基于TransactionInterceptor的事务业务逻辑
1 @Service("testDaoInterceptor") 2 3 public class TestDaoInterceptor implements TestDao{ 4 5 6 7 @Resource 8 9 private DataSource dataSource; 10 11 12 13 public void insert(String key, Object value) throws Exception { 14 15 try { 16 17 JdbcTemplate jt = new JdbcTemplate(dataSource); 18 19 int ret1 = jt.update("insert into kv (k, v)" 20 21 + " values('" + key + "', '" + value + "')"); 22 23 System.out.println("insert first time. ret1 = " + ret1); 24 25 int ret2 = jt.update("insert into kv (k, v)" 26 27 + " values('" + key + "', '" + value + "')"); 28 29 System.out.println("insert second time.ret2 = " + ret2); 30 31 32 33 } catch (Exception e) { 34 35 36 37 System.out.println("caught exception: --------"); 38 39 e.printStackTrace(); 40 41 throw e; 42 43 } 44 45 } 46 47 }
清单7:配置文件
1 <bean id="txInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> 2 3 <property name="transactionManager" ref="txManager"></property> 4 5 <property name="transactionAttributes"> 6 7 <props> 8 9 <prop key="insert">PROPAGATION_REQUIRED</prop> 10 11 </props> 12 13 </property> 14 15 </bean> 16 17 <bean id="testDaoInterceptorBean" class="org.springframework.aop.framework.ProxyFactoryBean"> 18 19 <property name="target" ref="testDaoInterceptor"/> 20 21 <property name="interceptorNames"> 22 23 <list> 24 25 <idref bean="txInterceptor"/> 26 27 </list> 28 29 </property> 30 31 </bean>
使用这种方式配置声明式事务,需要先配置一个TransactionInterceptor来定义相关的事务规则。它主要包括了两个属性,一个是transactionManager,指定一个事务管理器,transactionInterceptor将拦截到的事务相关操作委托给它。另一个是Properties类型的transactionAttributes属性,主要用来定义事务规则, 这个属性的具体配置不在这里详述。前面已经说过这种配置事务的方式是基于spring AOP的,从TransactionInterceptor的类继承关系中可以看到,这个类是继承自Advice接口的,熟悉spring AOP的同学应该知道AOP除了需要配置Advice之外,还需要一个ProxyFactoryBean来组装target 和 advice,通过spring工厂获取proxyFactoryBean实例时,其实返回的是proxyFactoryBean实例getObject返回的对象,也就是织入了事务管理逻辑后的目标类的代理类实例。这种方式的事务实现没有对业务代码进行任何操作,所有设置均在配置文件中完成。但是这种方式也存在一个烦人的问题:配置文件太长。需要为每个目标对象配置一个proxyFactoryBean和一个transactionInterceptor,相当于每个业务类需要配置3个bean,随着业务类增多,配置文件会越来越庞大,管理变得复杂。
2) 基于TransactionProxyFactoryBean的声明式事务管理
为了缓解ProxyFactoryBean实现声明式事务配置繁杂的问题,spring提供了TransactionProxyFactoryBean,将ProxyFactoryBean和TransactionInterceptor的配置打包成一个,其它的东西根本没有改变。示例配置如下:
清单8:基于TransactionProxyFactoryBean的配置文件
1 <bean id="testDaoTxBean" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> 2 3 <property name="target" ref="testDaoInterceptor"/> 4 5 <property name="transactionManager" ref="txManager"></property> 6 7 <property name="transactionAttributes"> 8 9 <props> 10 11 <prop key="*">PROPAGATION_REQUIRED</prop> 12 13 </props> 14 15 </property> 16 17 </bean>
这种配置被称为spring经典的声明式事务管理,虽然这种方式比起使用ProxyFactoryBean并没有什么大的改进,其实这两种方式对于配置声明式事务已经足够简单。
3) 基于<tx>命名空间的声明式事务管理
前面的两种方式已经很好的使用了AOP来实现事务管理,此外,spring还提供了一种引入<tx>命名空间,结合使用<aop>命名空间,带给开发人员配置声明式事务的全新体验(这个太扯蛋了,没什么意思嘛)。
清单9:基于<tx>命名空间的事务管理配置文件
1 <tx:advice id="daoAdvice" transaction-manager="txManager"> 2 3 <tx:attributes> 4 5 <tx:method name="insert" propagation="REQUIRED"></tx:method> 6 7 </tx:attributes> 8 9 </tx:advice> 10 11 <aop:config> 12 13 <aop:pointcut expression="execution(* *.insert(..))" id="daoPointcut"/> 14 15 <aop:advisor advice-ref="daoAdvice" pointcut-ref="daoPointcut"/> 16 17 </aop:config>
这种方式有点好处,就是不需要指定具体的被代理的业务类,这样就只需要合理的配置切点的表达式,然后只要满足条件的业务类都将被代理。
4) 基于@Transactional注解的声明式事务管理
Spring中最简便的配置方式当然要属注解方式,声明式事务管理也不例外,spring使用@Transactional注解作用在接口、接口方法、类和类方法上,一般使用@Transactional在业务类public方法上,这种方式简单明了,没有学习成本。
总的来说,这四种声明式事务管理只是使用形式不同,其后台的实现方式是相同的。
四、结篇总结
4.1 遇到问题
使用ProxyFactoryBean和TransactionProxyFactoryBean实现声明式事务管理类时,发现事务不生效。多次试验后发现示例中的业务逻辑代码把异常都捕获并处理,相对于事务来说,业务没有抛出异常,所以事务会被提交而插入了一条数据。使用这种方式处理事务时切记要将异常捕获放开,让事务检测到异常并针对性的处理事务。
4.2 知识总结
1)编程式事务使用的三个接口。
2)两种编程式事务实现和四种声明式事务实现。