smalllove 2019-10-24
理解事务之前,先讲一个你日常生活中最常干的事:取钱。
比如你去ATM机取1000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉1000元钱;然后ATM出1000元钱。这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。所以,如果一个步骤成功另一个步骤失败对双方都不是好事,如果不管哪一个步骤失败了以后,整个取钱过程都能回滚,也就是完全取消所有操作的话,这对双方都是极好的。
事务就是用来解决类似问题的。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。
在企业级应用程序开发中,事务管理必不可少的技术,用来确保数据的完整性和一致性。
事务有四个特性:ACID
Spring事务管理包含两种情况,编程式事务、声明式事务。而声明式事务又包括基于注解@Transactional和tx+aop的方式。编程式事务管理使用TransactionTemplate或者PlatformTransactionManager。 编程式事务需要手动在代码中处理事务,项目中使用的较少,也不推荐使用,因为它直接耦合代码,这里就不多介绍了,本文主要讲解申明式事务
我们将之前的项目改造,在Spring配置文件applicationContext.xml中屏蔽AOP及TX部分
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!--加载上下文注解配置--> <context:annotation-config/> <context:property-placeholder location="classpath:Resources/db.properties"/> <context:component-scan base-package="com.founderit"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <aop:aspectj-autoproxy/> <!--DBCP数据库连接池--> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" > <property name="driverClassName" value="${DRIVERCLASS}"/> <property name="url" value="${URL}"/> <property name="username" value="${USERNAME}"/> <property name="password" value="${PASSWORD}"/> <!--maxActive: 最大连接数量--> <property name="maxTotal" value="150"/> <!--minIdle: 最小空闲连接--> <property name="minIdle" value="5"/> <!--maxIdle: 最大空闲连接--> <property name="maxIdle" value="20"/> <!--initialSize: 初始化连接--> <property name="initialSize" value="30"/> <!--removeAbandonedTimeout: 超时时间(以秒数为单位)--> <property name="removeAbandonedTimeout" value="10"/> </bean> <!-- spring和MyBatis完美整合 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 自动扫描mapping.xml文件 --> <property name="mapperLocations" value="classpath:com/founderit/mapper/*.xml"></property> </bean> <!-- DAO接口所在包名,Spring会自动查找其下的类 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.founderit.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <!--事务管理部分--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" /><!-- <tx:advice id="txAdvice" transaction-manager="transactionManager">--><!-- <tx:attributes>--><!-- <tx:method name="save*" propagation="REQUIRED" />--><!-- <tx:method name="insert*" propagation="REQUIRED" />--><!-- <tx:method name="add*" propagation="REQUIRED" />--><!-- <tx:method name="create*" propagation="REQUIRED" />--><!-- <tx:method name="delete*" propagation="REQUIRED" />--><!-- <tx:method name="update*" propagation="REQUIRED" />--><!-- <tx:method name="find*" propagation="SUPPORTS" read-only="true" />--><!-- <tx:method name="select*" propagation="SUPPORTS" read-only="true" />--><!-- <tx:method name="get*" propagation="SUPPORTS" read-only="true" />--><!-- </tx:attributes>--><!-- </tx:advice>--><!-- <aop:config>--><!-- <aop:pointcut id="iccardTerm" expression="execution(* com.founderit.service.*.*(..))" />--><!-- <aop:advisor pointcut-ref="iccardTerm" advice-ref="txAdvice" />--><!-- </aop:config>--></beans>
在Service层类上加@Transactional注解,并在catch错误中手动回滚事务
@Transactional@Overridepublic int insertObject(Object obj) { int result=0; try { for (int i=0;i<4;i++) { Student student=new Student(); student.setName("wll"); student.setAge(1); student.setId(100+i); result=userDao.insertObject(student); } }catch (Exception e){ TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return result;}
测试
项目中将插入四条记录,我们在插入第三条时手动报错,如果没有事务,前二条正常的记录将插入成功,运行tomcat,发起hello请求
@Transactional@Overridepublic int insertObject(Object obj) { int result=0; try { for (int i=0;i<4;i++) { Student student=new Student(); student.setName("wll"); student.setAge(1); student.setId(100+i); if (i==2){ int error=10/0; } result=userDao.insertObject(student); } }catch (Exception e){ TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return result;}执行日志如下,可以看到前二条记录正确执行,但是插入第三条时报错,事务进行了回滚,导致一条也没有插入成功
在Spring配置文件applicationContext.xml中加上AOP及TX部分
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!--加载上下文注解配置--> <context:annotation-config/> <context:property-placeholder location="classpath:Resources/db.properties"/> <context:component-scan base-package="com.founderit"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <aop:aspectj-autoproxy/> <!--DBCP数据库连接池--> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" > <property name="driverClassName" value="${DRIVERCLASS}"/> <property name="url" value="${URL}"/> <property name="username" value="${USERNAME}"/> <property name="password" value="${PASSWORD}"/> <!--maxActive: 最大连接数量--> <property name="maxTotal" value="150"/> <!--minIdle: 最小空闲连接--> <property name="minIdle" value="5"/> <!--maxIdle: 最大空闲连接--> <property name="maxIdle" value="20"/> <!--initialSize: 初始化连接--> <property name="initialSize" value="30"/> <!--removeAbandonedTimeout: 超时时间(以秒数为单位)--> <property name="removeAbandonedTimeout" value="10"/> </bean> <!-- spring和MyBatis完美整合 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 自动扫描mapping.xml文件 --> <property name="mapperLocations" value="classpath:com/founderit/mapper/*.xml"></property> </bean> <!-- DAO接口所在包名,Spring会自动查找其下的类 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.founderit.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <!--事务管理部分--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" /> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="insert*" propagation="REQUIRED" /> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="create*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="find*" propagation="SUPPORTS" read-only="true" /> <tx:method name="select*" propagation="SUPPORTS" read-only="true" /> <tx:method name="get*" propagation="SUPPORTS" read-only="true" /> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="iccardTerm" expression="execution(* com.founderit.service.*.*(..))" /> <aop:advisor pointcut-ref="iccardTerm" advice-ref="txAdvice" /> </aop:config></beans>
service层
@Override
public int insertObject(Object obj) { int result=0; for (int i=0;i<4;i++) { Student student=new Student(); student.setName("wll"); student.setAge(1); student.setId(100+i); result=userDao.insertObject(student); } return result;}
测试
项目中将插入四条记录,我们在插入第三条时手动报错,如果没有事务,前二条正常的记录将插入成功,运行tomcat,发起hello请求
@Override
public int insertObject(Object obj) { int result=0; for (int i=0;i<4;i++) { Student student=new Student(); student.setName("wll"); student.setAge(1); student.setId(100+i); if (i==2){ int error=10/0; } result=userDao.insertObject(student); } return result;}
需要注意的是spring aop被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获runtimeException的异常
如service层做了异常处理,事务不会回滚,我们将service层做了try/catch处理,再次测试发现事务并没有回滚而是正常提交了
解决方案:
service层处理事务,那么service中的方法中不做异常捕获(推荐),或者在catch语句中最后增加throw new RuntimeException()语句,以便让aop捕获异常再去回滚,并且在service上层(controller)要继续捕获这个异常并处理