引起Spring事务失效的可能原因
一、 事务不生效
1.访问权限问题
众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:
@Service public class RoleService { @Transactional private void add(RoleModel roleModel) { save(roleModel); update(roleModel); } }
AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
Don't allow no-public methods as required.
也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。
2. 方法用final修饰
@Service public class RoleService { @Transactional public final void add(RoleModel roleModel){ save(roleModel); update(roleModel); } }
结果:事务失效。
为什么?
如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.方法内部调用
@Service public class UserService { @Autowired private UserMapper userMapper; public void add(UserModel userModel) { userMapper.insert(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); } }
结果是正确还是失败呢?
答案:在同一个类中的方法直接内部调用,会导致事务失效。
那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
3.1 新加一个Service方法
@Service public class ServiceA { @Autowired prvate ServiceB serviceB; public void save(User user) { queryData1(); queryData2(); serviceB.doSave(user); } } @Service public class ServiceB { @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }
事务是否生效?答案:是可以的
3.2 在该Service类中注入自己
@Service public class ServiceA { @Autowired prvate ServiceA serviceA; public void save(User user) { queryData1(); queryData2(); serviceA.doSave(user); } @Transactional(rollbackFor = Exception.class) public void doSave(User user) { addData1(); updateData2(); } }
事务是否生效?答案:是可以的
这种做法会不会出现循环依赖问题?答案:不会。
spring ioc内部的三级缓存保证了它,不会出现循环依赖问题
3.3 通过AopContent类
@Service public class ServiceA { public void save(User user) { queryData1(); queryData2(); ((ServiceA) AopContext.currentProxy()).doSave(user); } @Transactional(rollbackFor = Exception.class) public void doSave(User user) { addData1(); updateData2(); } }
事务是否生效?答案:是可以的
3.4 同类中调用嵌套方法AOP失效
在同一个类中,非事务方法A调用事务方法B,会导致事务失效,可以采用AopContext.currentProxy().xxxxx()来保证事务生效。
注意事项:
Spring Boot需要在启动类加上以下注解
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
SSM需要xml文件配置
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
为什么AopContext可以解决同类方法AOP失效问题
AopContext类的源码如下
package org.springframework.aop.framework; import org.springframework.core.NamedThreadLocal; import org.springframework.lang.Nullable; public final class AopContext { // 维护了一个ThreadLocal,存放AOP代理类 private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy"); private AopContext() { } public static Object currentProxy() throws IllegalStateException { Object proxy = currentProxy.get(); if (proxy == null) { throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and " + "ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context."); } return proxy; } // 提供代理类set进ThreadLocal方法 @Nullable static Object setCurrentProxy(@Nullable Object proxy) { Object old = currentProxy.get(); if (proxy != null) { currentProxy.set(proxy); } else { currentProxy.remove(); } return old; } } //jdk动态代理创建时JdkDynamicAopProxy中的invoke方法中调用AopContext存入代理类 public Object getProxy() { return getProxy(ClassUtils.getDefaultClassLoader()); } public Object getProxy(ClassLoader classLoader) { if (logger.isDebugEnabled()) { logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource()); } Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); } //生成代理类,反射生成的。JdkDynamicAopProxy继承自InvocationHandler public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; TargetSource targetSource = this.advised.targetSource; Object target = null; Integer var8; try { if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { Boolean var18 = this.equals(args[0]); return var18; } if (this.hashCodeDefined || !AopUtils.isHashCodeMethod(method)) { if (method.getDeclaringClass() == DecoratingProxy.class) { Class var17 = AopProxyUtils.ultimateTargetClass(this.advised); return var17; } Object retVal; if (!this.advised.opaque && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) { retVal = AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); return retVal; } if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } target = targetSource.getTarget(); Class<?> targetClass = target != null ? target.getClass() : null; List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); if (chain.isEmpty()) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); retVal = invocation.proceed(); } Class<?> returnType = method.getReturnType(); if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { retVal = proxy; } else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method); } Object var12 = retVal; return var12; } var8 = this.hashCode(); } finally { if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { AopContext.setCurrentProxy(oldProxy); } } return var8; } 请注意关键代码 if (this.advised.exposeProxy) { oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } cglib动态代理CglibAopProxy在创建代理时也调用AopContext存入代理类 private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable { private final AdvisedSupport advised; public DynamicAdvisedInterceptor(AdvisedSupport advised) { this.advised = advised; } @Override @Nullable public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; Object target = null; TargetSource targetSource = this.advised.getTargetSource(); try { if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool... target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object retVal; // Check whether we only have one InvokerInterceptor: that is, // no real advice, but just reflective invocation of the target. if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { // We can skip creating a MethodInvocation: just invoke the target directly. // Note that the final invoker must be an InvokerInterceptor, so we know // it does nothing but a reflective operation on the target, and no hot // swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = methodProxy.invoke(target, argsToUse); } else { // We need to create a method invocation... retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } retVal = processReturnType(proxy, target, method, retVal); return retVal; } finally { if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } } 其中关键代码 if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; }
总结:AopContext可以解决同类方法AOP失效问题
4.未被spring管理
@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
忘了加@Service注解(错误用法):
//@Service public class UserService { @Transactional public void add(UserModel userModel) { save(userModel); update(userModel); } }
事务是否生效?答案是不会生成事务。
5.多线程调用(注意)
spring事务用在多线程场景中,会有问题吗?
事务是否生效?(错误用法):
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insert(userModel); new Thread(() -> { roleService.doOtherThing(); }).start(); } } @Service public class RoleService { @Transactional public void doOtherThing() { System.out.println("保存role表数据"); } }
事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
二 、事务不回滚
spring目前支持7种传播特性:
- REQUIRED如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
- SUPPORTS如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
- MANDATORY如果当前上下文中存在事务,否则抛出异常。
- REQUIRES_NEW每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
- NOT_SUPPORTED如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
- NEVER如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
- NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
@Service public class UserService { @Transactional(propagation = Propagation.NEVER) public void add(UserModel userModel) { save(userModel); update(userModel); } }
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
2.自己吞了异常
事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。比如(错误用法):
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) { try { save(userModel); update(userModel); } catch (Exception e) { log.error(e.getMessage(), e); } } }
这种情况下spring事务当然不会回滚
3.手动抛了别的异常
异常是否会回滚?(错误用法)
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) throws Exception { try { save(userModel); update(userModel); } catch (Exception e) { log.error(e.getMessage(), e); throw new Exception(e); } } }
答案:普通的Exception(非运行时异常),它不会回滚。
即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。
上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。
因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
4.自定义了回滚异常(建议使用Exception或Throwable)
在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。
异常是否会回滚?(错误用法)
@Slf4j @Service public class UserService { @Transactional(rollbackFor = BusinessException.class) public void add(UserModel userModel) throws Exception { save(userModel); update(userModel); } }
答案:事务也不会回滚。
如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。
因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。
5.嵌套事务回滚多了
public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insert(userModel); roleService.doOtherThing(); } } @Service public class RoleService { @Transactional(propagation = Propagation.NESTED) public void doOtherThing() { System.out.println("保存role表数据"); } }
这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。
因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
怎么样才能只回滚保存点呢?
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insert(userModel); try { roleService.doOtherThing(); } catch (Exception e) { log.error(e.getMessage(), e); } } }
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
三、 其他
1 大事务问题
package com.alibaba.fulfill.domain.event; @Service public class UserService { @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { query1(); query2(); query3(); roleService.save(userModel); update(userModel); } } @Service public class RoleService { @Autowired private RoleService roleService; @Transactional public void save(UserModel userModel) throws Exception { query4(); query5(); query6(); saveData(userModel); } }
其实只有这两行才需要事务:
roleService.save(userModel); update(userModel);
在RoleService类中,只有这一行需要事务:
saveData(userModel);
如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。
总结:
相较于@Transactional注解声明式事务,我们更建议大家使用,基于TransactionTemplate的编程式事务。主要原因如下:
- 避免由于spring aop问题,导致事务失效的问题。
- 能够更小粒度的控制事务的范围,更直观。
- 建议一般情况下,将该参数设置成:Exception或Throwable