Spring事务失效十种原因
本文详细的介绍了Spring事务失效的十种原因,掌握住它们并在工作避免这些情况。
一、类未被spring管理
// @Service
public class AServiceImpl implements AService {
@Transactional
public void updateA(A a) {
// update a
}
}
将 @Service注解注释后,该类就不受 Spring容器管理,导致事务失效!这是因为 Spring事务是由 AOP机制实现的,AOP机制的本质就是动态代理,从 Spring IOC容器获取 bean时,Spring会为目标类创建代理,从而支持事务。
二、事务方法不是 public
spring 源码:
public abstract class AbstractFallbackTransactionAttributeSource
implements TransactionAttributeSource, EmbeddedValueResolverAware {
// 获取事务属性
@Override
@Nullable
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 部分代码省略
if (cached != null) {
// 部分代码省略
}
else {
// 调用 计算事务属性 方法
// We need to work it out.
TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
// 部分代码省略
return txAttr;
}
}
// 计算事务属性
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 按照配置,不允许使用非公共方法。
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// 部分代码省略
return null;
}
}
我们可以看到add方法的访问权限被定义成了private
,这样会导致事务失效。从spring源码中可以看出被代理方法必须是public
的。在AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则TransactionAttribute
返回null,即不支持事务。
三、方法用final修饰
@Service
public class AService {
@Transactional
public final void add(Model model){
saveData(model);
int i = 1/0;
}
}
spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,添加事务功能。
四、方法内部调用事务方法
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser(User user){
this.addUser(user);
}
@Transactional(rollbackFor = Exception.class)
public void addUser (User user){
userMapper.save(user);
int i = 1/0;
}
}
数据保存成功
为什么事务没有回滚?
事务是通过Spring AOP代理来实现的,而 addUser()内部事务方法其实是this对象调用的,而不是通过AOP代理来调用的,因此事务失效。
五、异常被 catch
@Service
public class UserServiceImpl implements UserService {
@Transactional
@Override
public void addUser(User user) {
try{
userMapper.save(user);
// 模拟异常,数据库的记录应该回滚
throw new RuntimeException();
} catch (RuntimeException e) {
}
}
}
数据保存成功
为什么事务没有回滚?
我们来看看 Spring的官方核心源码:
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 省略部分代码
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// target invocation exception
// 回滚事务 是在 spring 的catch中处理,也就是说,如果Spring catch不到对应的异常,就不会进入回滚事务的逻辑
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
// 省略部分代码
// 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
/**
* Handle a throwable, completing the transaction.
* We may commit or roll back, depending on the configuration.
* @param txInfo information about the current transaction
* @param ex throwable encountered
*/
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
// 省略部分代码
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 回滚事务
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 省略部分代码
}
}
}
通过源码,我们可以很清晰的看出,在 invokeWithinTransaction() 方法中,当 Spring catch到 Throwable异常,就会调用 completeTransactionAfterThrowing()方法进行事务回滚的逻辑。但是,在 UserServiceImpl类的业务代码中直接把异常catch住了,Spring自然就 catch不到异常,因此事务回滚的逻辑就不会执行,事务就失效了。
六、表不支持事务
Spring 事务是业务层的事务,其底层还是依赖于数据库本身的事务支持。比如 MySQL 数据库,MyISAM 引擎不支持事务而 InnoDB 引擎支持事务。所以,开发中如果需要使用事务,一定要确保你选择的数据库支持事务。
七、rollbackFor属性配置错误
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser(User user){
// 调用内部方法
this.doAddUser(user);
}
@Transactional(rollbackFor = Error.class)
public void addUserInfo (User user){
userMapper.save(user);
// 模拟异常,数据库的记录应该回滚
throw new RuntimeException();
}
}
数据保存成功
为什么事务没有回滚?
我们依旧来看看 Spring的官方核心源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
/**
* Defines zero (0) or more exception classes, which must be subclasses of Throwable, indicating which exception types must cause a transaction rollback.
* By default, a transaction will be rolled back on RuntimeException and Error but not on checked exceptions (business exceptions). See org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable) for a detailed explanation.
* This is the preferred way to construct a rollback rule (in contrast to rollbackForClassName), matching the exception type, its subclasses, and its nested classes. See the class-level javadocs for further details on rollback rule semantics and warnings regarding possible unintentional matches.
* @return
*/
// rollbackFor的异常必须是 Throwable的子类
Class<? extends Throwable>[] rollbackFor() default {};
}
通过 Transactional注解源码,我们可以发现 rollbackFor属性指定的异常必须是 Throwable及其子类,并且在默认情况下,Spring对 RuntimeException 和 Error 两种异常会自动回滚事务,也就是说,如果业务抛出来的异常是 RuntimeException 和 Error类型,可以不需要通过 rollbackFor属性指定,Spring 默认会识别处理。代码中抛出的异常是 Exception(throw new Exception()),而 Exception 和 Error没有任何关系,也就是说,事务需要捕获到 Error才会回滚,可是抛出一个和 Error不相关的 Exception异常,因此事务自然无效,不能回滚。
八、错误的事物传播特性
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
spring目前支持7种传播特性:
-
REQUIRED
如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。 -
SUPPORTS
如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。 -
MANDATORY
如果当前上下文中存在事务,否则抛出异常。 -
REQUIRES_NEW
每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。 -
NOT_SUPPORTED
如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。 -
NEVER
如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。 -
NESTED
如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
九、多线程调用
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.save();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void save() {
// 保存数据
}
}
我们可以看到事务方法add中,调用了事务方法save,但是事务方法save是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果save方法中抛了异常,add方法也回滚是不可能的。如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
十、未开启事务
事务没有生效,原因有很多,但没有开启事务,这个原因极其容易被忽略。
如果你使用的是springboot项目,那么springboot通过DataSourceTransactionManagerAutoConfiguration
类,已经默默的帮你开启了事务。
你所要做的事情很简单,只需要配置spring.datasource
相关参数即可。
但如果你使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
具体配置如下信息:
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/></tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config> <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
更多推荐
所有评论(0)