最近在生产项目上出现一些问题,同一流程下涉及到多个数据库表的增改出现不一致的情况;

例如tableA,tableB,tableC:


三张表同时做insert操作(或者是update操作),其中tableA,tableB保存成功,tableC却未能保存成功;这样的话,就造成生产服务器上的数据不准确;

系统环境:spring3.0.2+struts2.18+hibernate3.3.2

解决方案:

使用的是spring框架;所以想到的肯定是使用spring整合hibernate的事务管理机制



因为这个系统已经开发了一段时间,这个框架中也添加了spring事务管理机制,但是问题是没有生效,这里就说一下如何解决事务没有生效的问题;

one:(贴配置文件)

ApplicationContext.xml
<!-- 定义事务管理器 --> <bean id="txManage"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" /> </bean>
<!--<tx:annotation-driven transaction-manager="txManage"/>--> <tx:advice
id="txAdvice" transaction-manager="txManage"> <tx:attributes> <!--注意
propagation="REQUIRED"... --> <tx:method name="insert*" propagation="REQUIRED"
rollback-for="com.rhxy.utils.SelfException"/> <tx:method name="doA*"
propagation="REQUIRED" rollback-for="com.rhxy.utils.SelfException"/> <tx:method
name="test*" propagation="REQUIRES_NEW" rollback-for="com.rhxy
.utils.SelfException"/> <tx:method name="get*" read-only="true" /> <tx:method
name="find*" read-only="true" /> <tx:method name="search*" read-only="true" />
<tx:method name="query*" read-only="true" /> <tx:method name="add*"
propagation="REQUIRED" /> <tx:method name="del*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" /> <tx:method name="do*"
propagation="REQUIRED" /> <tx:method name="save*" propagation="REQUIRED" />
<tx:method name="saveOrUpdate*" propagation="REQUIRED" /> <!--<tx:method
name="*" propagation="REQUIRED" read-only="true" />--> <tx:method name="*"
propagation="SUPPORTS" /> </tx:attributes> </tx:advice> <aop:aspectj-autoproxy
proxy-target-class="true" /> <aop:config> <!--注意 aop:pointcut 定义切入点-->
<aop:pointcut expression="execution(*com.rhxy.dao.*.*(..))||execution(* com.rhxy
.service.inventory.StockService.test(..)) || execution(*com.rhxy
.action.InStoreRecordAction.*(..))" id="serviceMethod" /> <aop:advisor
advice-ref="txAdvice" pointcut-ref="serviceMethod" /> </aop:config>


在这里必须要注意的地方有两点:

1.Spring事务的传播机制
Propagation
:  key属性确定代理应该给哪个方法增加事务行为。这样的属性最重要的部份是传播行为。有以下选项可供使用:PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。 1: PROPAGATION_REQUIRED
加入当前正要执行的事务不在另外一个事务里,那么就起一个新的事务
比如说,ServiceB.methodB的事务级别定义为PROPAGATION_REQUIRED, 那么由于执行ServiceA.methodA的时候,
ServiceA.methodA已经起了事务,这时调用ServiceB.methodB,ServiceB.methodB看到自己已经运行在ServiceA.methodA
的事务内部,就不再起新的事务。而假如ServiceA.methodA运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在ServiceA.methodA或者在ServiceB.methodB内的任何地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被
提交,但是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚 2: PROPAGATION_SUPPORTS
如果当前在事务中,即以事务的形式运行,如果当前不再一个事务中,那么就以非事务的形式运行 3: PROPAGATION_MANDATORY
必须在一个事务中运行。也就是说,他只能被一个父事务调用。否则,他就要抛出异常 4: PROPAGATION_REQUIRES_NEW 这个就比较绕口了。
比如我们设计ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW,
那么当执行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的事务,等待ServiceB.methodB的事务完成以后,
他才继续执行。他与PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为ServiceB.methodB是新起一个事务,那么就是存在
两个不同的事务。如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。如果ServiceB.methodB失败回滚,
如果他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。 5:
PROPAGATION_NOT_SUPPORTED 当前不支持事务。比如ServiceA.methodA的事务级别是PROPAGATION_REQUIRED
,而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,
那么当执行到ServiceB.methodB时,ServiceA.methodA的事务挂起,而他以非事务的状态运行完,再继续ServiceA.methodA的事务。
6: PROPAGATION_NEVER 不能在事务中运行。假设ServiceA.methodA的事务级别是PROPAGATION_REQUIRED,
而ServiceB.methodB的事务级别是PROPAGATION_NEVER , 那么ServiceB.methodB就要抛出异常了。 7:
PROPAGATION_NESTED
理解Nested的关键是savepoint。他与PROPAGATION_REQUIRES_NEW的区别是,PROPAGATION_REQUIRES_NEW另起一个事务,将会与他的父事务相互独立,
而Nested的事务和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,他也要回滚的。
而Nested事务的好处是他有一个savepoint。


2.Spring AOP pointcut的用法
1.什么是AOP? Aspect Orentied Programming (AOP,面向方面编程) Object Orentied Programming
(OOP,面向对象编程) AOP编程是以OOP为基础,OOP侧重点是对象抽象和封装, AOP侧重点是共通处理部分的封装和使用.用于改善共通组件
和目标组件之间的关系(低耦合) 2.AOP使用示例 ------AOP示例操作步骤------ a.引入Spring-AOP的开发包.
b.首先编写一个方面组件,将共通处理封装. c.然后在Spring容器配置中添加AOP定义
--将方面组件Bean定义,采用<aop:aspect>指定为方面组件 --采用<aop:pointcut>指定切入点,确定目标对象
--采用<aop:after>或<aop:before>通知指定方面组件和目标对象方法的作用时机. 3.AOP相关概念 *a.Aspect(方面组件)
方面组件就是封装共通处理逻辑的组件,其功能将来要作用到某一批目标方法上.例如日志记录,异常处理,事务处理等 *b.PointCut(切入点)
切入点是用于指定目标对象或方法的一个表达式. c.JointPoint(连接点) 切入点是连接点的集合.指的是方面组件和目标组件作用的位置.
例如方法调用,异常发生位置. *d.Advice(通知)
用于指定方面组件在目标方法上作用的时机.例如在目标方法之前执行,目标方法之后执行,目标方法之前和之后执行等. e.Target(目标对象)
要使用方面功能的组件对象.或被切入点表达式指定的对象 f.AutoProxy(动态代理对象) Spring使用了AOP机制后,采用的是动态代理技术实现的.
当采用了AOP之后,Spring通过getBean返回的对象是一个动态代理类型对象.当使用该对象的业务方法时,该对象会负责调用方面组件和目标组件的功能.
如果未采用AOP,Spring通过getBean返回的是原始类型对象,因此执行的是原有目标对象的处理. Spring动态代理技术采用的是以下两种:
--采用JDK Proxy API实现.(目标对象有接口定义) --采用Cglib.jar工具包API实现.(目标对象没有接口定义) 4.通知类型
通知主要负责指定方面功能和目标方法功能的作用关系. Spring框架提供了以下5种类型通知. a. 前置通知<aop:before>
方面功能在目标方法之前调用. b. 后置通知<aop:after-returning> 方面功能在目标方法之后调用.目标方法无异常执行. c. 最终通知
<aop:after> 方面功能在目标方法之后调用.目标方法有无异常都执行 e. 异常通知 <aop:after-throwing>
方面功能在目标方法抛出异常之后执行. f. 环绕通知 <aop:around> 方面功能在目标方法执行前和后调用. try{ //环绕通知前置部分功能
//前置通知--执行方面功能 调用目标方法处理 //后置通知--执行方面功能 //环绕通知后置部分功能 }catch(){ //异常通知--执行方面功能
}finally{ //最终通知--执行方面功能 } 5.切入点表达式的指定 切入点表达式用于指定哪些对象和方法调用方面功能. *1)方法限定表达式
execution(修饰符? 返回类型 方法名(参数) throws 异常类型? ) 示例1--匹配所有Bean对象中以add开头的方法
execution(* add*(..)) 示例2--匹配UserService类中所有的方法 execution(*
tarena.service.UserService.*(..)) 示例3--匹配UserService类中有返回值的所有方法 execution(!void
tarena.service.UserService.*(..)) 示例4--匹配所有Bean对象中修饰符为public,方法名为add的方法
execution(public * add(..)) 示例5--匹配tarena.service包下所有类的所有方法 execution(*
tarena.service.*.*(..)) 示例6--匹配tarena.service包及其子包中所有类所有方法 execution(*
tarena.service..*.*(..)) *2)类型限定 within(类型) 示例1--匹配UserService类中的所有方法
within(tarena.service.UserService) 示例2--匹配tarena.service包下所有类型的所有方法
within(tarena.service.*) 示例3--匹配tarena.service包及其子包中所有类型的所有方法
within(tarena.service..*) 3)Bean名称限定 bean(bean的id|name属性值)
按<bean>元素定义的id或name值做匹配限定. 示例1--匹配容器中bean元素id="userService"对象 bean(userService)
示例2--匹配容器中所有id属性以Service结束的bean对象 bean(*Service) 4)参数列表限定 arg(参数类型)
示例--匹配有且只有一个String参数的方法 arg(java.lang.String)



two:(不生效有哪些因素)

1.数据库原因,因为有的数据库引擎是不支持事务管理的。如果你用的是mysql数据库,看看数据库使用的是什么引擎
使用下述语句之一检查表的标类型: SHOW TABLE STATUS LIKE 'tableName'; SHOW CREATE TABLE
tableName;

2.使用的是Spring+mvc框架,有可能是因为spring配置的自动扫描重复扫描所造成的,仔细检查一下spring配置文件;或者是因为加载Spring配置文件时,按照Spring配置文件的加载顺序,先加载SpringMVC的配置,再加载Spring的配置,一般情况下我们的事务管理都是配置在Spring的配置文件,而先加载SpringMVC时,把Service也注册了,但是这个时候事务还没有加载,也就导致事务无法成功注入到Service中。
1、在主容器中(applicationContext.xml),将Controller的注解排除掉 <context:component-scan
base-package="com"> <context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan> 2、而在springMVC配置文件中将Service注解给去掉
<context:component-scan base-package="com"> <context:include-filter
type="annotation" expression="org.springframework.stereotype.Controller" />
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Service" /> </context:component-scan>
3.Aop的切入点出错了
使用如下代码 确认你的bean 是不是代理对象 AopUtils.isAopProxy() AopUtils.isCglibProxy() //cglib
AopUtils.isJdkDynamicProxy() //jdk动态代理
一般情况下
不使用AOP切面的话,所获得的bean是一个普通对象,也就是目标对象;如果使用了AOP,则相关对象是一个代理对象,通过三面三种方法验证你自己定义的aop:pointcut是否生效,如果生效的话,
AopUtils.isAopProxy() //返回true AopUtils.isCglibProxy() //cglib 返回true
AopUtils.isJdkDynamicProxy() //jdk动态代理 返回false
4.事务的传播属性配置错误,比如使用的是NEVER或者NOT_SUPPORTED;仔细检查配置文件,一般情况下使用REQUIRED、REQUIRES_NEW

5.注意控制台异常:
org.hibernate.HibernateException: save is not valid without active transaction
这是因为Spring整合的Hibernate配置中设置了
hibernate.current_session_context_class=true,获取session的方式是getCurrentSession()
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property
name="dataSource" ref="dataSource" /> <property name="hibernateProperties">
<props> <prop key="hibernate.connection.autocommit">true</prop> <prop
key="myeclipse.connection.profile">MySQL</prop> <!--<prop
key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>--> <prop
key="hibernate.dialect">com.rhxy.dao_new.MySQLLocalDialect</prop> <!--<prop
key="hibernate.hbm2ddl.auto">create</prop>--> <prop
key="hibernate.hbm2ddl.auto">update</prop> <prop
key="hibernate.myeclipse.connection.profile">true</prop> <!--<prop
key="hibernate.show_sql">true</prop>--> <!--<prop
key="hibernate.format_sql">true</prop>--><!--<prop
key="hibernate.current_session_context_class">thread</prop>这里我已经注释掉了--> <prop
key="hibernate.transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory </prop> <prop
key="hibernate.query.substitutions">true 1, false 0</prop> </props> </property>
<property name="mappingLocations"> <list>
<value>classpath:com/xxxx/bean/settings/*.hbm.xml</value>
<value>classpath:com/xxxx/bean/common/*.hbm.xml</value>
<value>classpath:com/xxxx/bean/customservice/*.hbm.xml</value>
<value>classpath:com/xxxx/bean/finance/*.hbm.xml</value> <!--...hbm.xml-->
</list> </property>

当调用getCurrentSession()时,hibernate将session绑定到当前线程,事务结束后mhibernate将session从当前线程中释放,并且关闭session。当再次调用时,得到一个新的session;

所以,将hibernate.current_session_context_class
的值设置为thread。当我们调用getCurrentSession()时,获取的不再是交由Spring托管的session了;所以获取的seesion并非事务管理器钟代理的那个session。所以不能自动开启事务。

不能使用openSession();这样的话虽然不会报错,但是事务不起作用

three:(解决思路)

1.使用AopUtils验证调用函数的对象是不是一个代理对象

这里说明一下:


因为在项目中,Action主要处理关于前端的一写操作,Service主要是处理业务逻辑,DAO是对数据库进行操作;因为我是药统一管理几个表的增删改,所以将事务管理放在Service中,但是真正的操作数据表是在DAO中;所以,要求Service每次都是打开一个新的Session(
REQUIRES_NEW),而DAO层,因为涉及多个表的操作,就需要使用同一个session,当session不存在时再创建(REQUIRED);
所以,这里需要在Action中验证Service是否是一个代理对象,在Service中验证DAO是否是一个代理对象;

如果验证返回false,那就是AOP定义的切点有问题;仔细检查,例如之前项目中在Service中获取到的是一个目标对象,并不是我们需要的代理对象,检查后发现:

execution(* com.xxxx.dao*.*(..))

修改为:


execution(* com.xxxx.dao.*.*(..))


再次验证,获取到的是代理对象;如果你要定义切点在Action上时,一定要在Spring配置文件中加入:
<aop:aspectj-autoproxy proxy-target-class="true" />
不然的话Action的代理对象会报找不到方法的异常

使用了Spring的事务管理后,在DAO层中就不需要手动提交


2.修改代码

添加Spring的事务传播属性,修改Aop pointcut, 修改session的获取方式,使用getCurrentSession()获取session,

修改Hibernate配置文件,删除current_session_context_class属性, 添加AOPUtils
测试代理对象(Action和Service中);

删除DAO层手动提交和回滚的代码

3.手动回滚或者手动抛出异常让Spring处理

以上都ok以后测试事务管理是否生效:

先按照正确流程走一遍(无报错的情况下);在事务结束以后数据库表中是否插入数据;如果正确,然后再测试不正确的流程(手动抛出异常或者运行异常),测试是否会回滚。

注意,即使你之前的所有的都配置好了,都没有问题了,还是有可能不生效:
Spring默认情况下会对运行期例外(RunTimeException)进行事务回滚。这个例外是unchecked 如果遇到checked意外就不回滚。
如何改变默认规则: 1 让checked例外也回滚:在Spring事务管理配置上加入rollback-for="java.lang.Exception" 2
让unchecked例外不回滚:在Spring事务管理配置上加入no-rollback-for="java.lang.Exception" 3
自定义异常回滚:在Spring事务管理配置上加入no-rollback-for="com.rhxy.Utils.SelfException"
所以在Service中,我们不捕获异常直接Throws Exception,这样的话
异常才能被Spring捕获到,注意不指定的话,默认是只有RunTimeException才生效

为了满足业务需求,我使用try{}catch;当出现异常我们捕获时,可以有两种方法让Spring事务管理器来处理:

A:在catch中手动抛出自己指定的异常或者是默认的RunTimeException,有Spring事务管理器来回滚

B:手动让事务进行回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

four:(结果)

经测试,Spring事务管理功能已正常;生产服务器中的数据不准确以及脏数据也已经解决









友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信