当前位置: 首页 > 科技观察

为什么Spring事务会失败?

时间:2023-03-14 15:20:13 科技观察

不要用Spring来管理事务?我们先看看在没有使用spring管理事务的情况下,各种框架是如何管理事务的。使用JDBC管理事务:使用Hibernate管理事务:业务逻辑和事务代码耦合在一起,绑定到框架特定的API上。当我们用另一个框架实现它时,事务控制的代码将被推翻和重写,这并不一定保证被替换的API与以前的API具有相同的行为。《统一事务抽象》Spring正是基于这些问题,对一些与事务相关的顶层接口进行了抽象。无论是全局事务还是局部事务,无论是JTA、JDBC还是Hibernate,Spring都采用了统一的编程模型。它使应用程序可以轻松地在全局事务和本地事务之间,或者在不同的事务框架之间切换。“下图是Spring对事物抽象的核心类。”常用的api接口PlatformTransactionManager管理事务。TransactionDefinition定义了事务的相关属性,例如隔离级别和传播行为。TransactionStatus保存交易状态。对于不同的数据访问技术,使用未使用的PlatformTransactionManager类。.数据访问技术PlatformTransactionManagerimplementsclassJDBC/MybatisDataSourceTransactionManagerHibernateHibernateTransactionManagerJpaJpaTransactionManagerJmsJmsTransactionManager程序化事务管理我们在使用Spring事务时,可以使用程序化事务,也可以使用声明式事务。使用程序化交易时,可以直接使用交易的顶层接口,也可以使用模板类TransactionTemplate。使用PlatformTransactionManager来使用TransactionTemplate当我们直接使用PlatformTransactionManager来管理事务时,有很多样板代码。比如业务代码执行正常,事务提交,否则事务回滚。我们可以将这部分模板代码封装成一个模板类,使用起来非常方便,如下图:如下图,TransactionTemplate#execute方法就是一个典型的模板方法:我们可以传入下面两个接口实现类来执行业务逻辑,TransactionCallback(需要返回执行结果)或TransactionCallbackWithoutResult(不需要返回结果)。为了让声明式事务管理的使用更加简洁,Spring直接将事务代码的执行放到了切面中。我们只需要在业务代码方法中添加一个@Transactional注解即可,这是最常用的方法。使用@Transactional注解,我们可以通过@Transactional注解来设置事务相关的定义。属性名称类型描述默认值值(与transactionManager别名)String当配置文件中有多个PlatformTransactionManager时,使用该属性指定选择哪个事务管理器空字符串""propagation枚举:Propagation事务传播行为示例:Isolation事务隔离DEFAULTtimeoutint事务超时。如果超过时间但事务还没有完成,自动回滚事务-1readOnlyboolean指定事务是否为只读事务falserollbackForClass[]需要回滚的异常emptyarray{}rollbackForClassNameString[]需要回滚的异常类名空数组{}noRollbackForClass[]不需要回滚的异常类空数组{}noRollbackForClassNameString[]不需要回滚的异常类名空数组{}源码分析我们需要在配置类中添加@EnableTransactionManagement注解,开启spring事务管理功能。“TransactionManagementConfigurationSelector#selectImports”将两个类AutoProxyRegistrar和ProxyTransactionManagementConfiguration注入到容器中。这两个类的作用是什么?(源码太多,就不贴代码一步步分析了,主要是理清思路)。AutoProxyRegistrar主要是在容器中注入一个类InfrastructureAdvisorAutoProxyCreator。这个类的作用是什么?“看继承关系,原来是继承自AbstractAutoProxyCreator,用来实现自动代理!”BeanFactoryTransactionAttributeSourceAdvisor主要是在容器中注入一个Advisor类,用于保存Pointcut和Advice。对应的Pointcut是TransactionAttributeSourcePointcut的实现类,是一个匿名内部类,即通过TransactionAttributeSourcePointcut类实现过滤逻辑。BeanFactoryTransactionAttributeSourceAdvisor对应的Advice的实现类是TransactionInterceptor,即事务增强的逻辑在这个类中。筛选的逻辑我们就不分析了,后面会简单提一下。让我们看看事务增强的逻辑。当执行@Transactional标记的方法时,会调用下面的方法(TransactionInterceptor#invoke有点类似于我们的@Around)。TransactionInterceptor#invokeTransactionAspectSupport#invokeWithinTransaction我挑出这个方法的几个重要部分来分析(上图圈出的部分)。必要时启动事务(与传播属性有关,我们稍后会提到)。执行业务逻辑。如果发生异常,事务将被滚动。如果正常执行则提交事务。“所以当出现异常需要roll的时候,一定不要尝试自己去捕获异常,否则交易会正常提交。”TransactionAspectSupport#createTransactionIfNecessary当你开始一个事务时,你可以看到各种传播属性的行为(即@Transactional方法调用@Transactional方法时会发生什么?)AbstractPlatformTransactionManager#getTransactionSpring事务的传播行为定义在Propagation枚举中类如下选择。“支持当前交易”必需:如果当前存在交易,请加入它。如果没有当前事务,则创建一个新事务。支持:如果当前存在交易,请加入它。如果没有当前事务,则以非事务方式继续运行。MANDATORY:如果当前存在事务,则加入它。如果没有当前事务,则抛出异常。"不支持当前事务"REQUIRES_NEW:如果有当前事务,则暂停当前事务并创建新事务。NOT_SUPPORTED:如果当前存在事务,则挂起当前事务并以非事务方式运行它。NEVER:如果事务当前存在则抛出异常。【其他情况】NESTED:如果有当前事务,创建一个事务作为当前事务的嵌套事务执行。如果没有当前事务,则此值等同于REQUIRED。以NESTED开头的事务嵌入到外部事务中(如果有外部事务)。这时候嵌入式事务就不是一个独立的事务了,它依赖于外部事务。只有通过外部事务的提交才能引起内部事务的提交,不能单独提交嵌套的子事务。交易失败的场景有哪些?因为我们经常使用声明式事务,如果消息的某个步骤会导致事务失败,那么我们从源码的角度来看一下事务失败的原因。异常被你捕获了。首先就是我们上面刚才说的,“异常被你捕获了”。因为声明式的东西通过目标方法是否抛出异常来决定是提交东西还是滚动东西。自调用自调用时,方法执行不会经过代理对象,因此会导致事务失败。//事务失效@ServicepublicclassUserServiceV2ImplimplementsUserService{@AutowiredprivateJdbcTemplatejdbcTemplate;@OverridepublicvoidaddUser(Stringname,Stringlocation){doAdd(name);}@TransactionalpublicvoiddoAdd(Stringname){Stringsql="insertintouser(`name`)values(?)";jdbcTemplate.update(sql,newObject[]{name});thrownewRuntimeException("保存用户失败");}}我们可以使用以下三种方式来解决自调用失败的场景。「1.@Autowired注册代理对象,然后调整使用方法」//@ServicepublicclassUserServiceV3ImplimplementsUserService{@AutowiredprivateJdbcTemplatejdbcTemplate;@AutowiredprivateUserService用户服务;@OverridepublicvoidaddUser(Stringname,Stringlocation){userService.doAdd(name);}@Override@TransactionalpublicvoiddoAdd(Stringname){Stringsql="insertintouser(`name`)values(?)";jdbcTemplate.update(sql,newObject[]{name});thrownewRuntimeException("保存用户丢失");}}「2.从ApplicationContext获取代理对象,然后调整使用方法」@ServicepublicclassUserServiceV4ImplimplementsUserService{@AutowiredprivateJdbcTemplatejdbcTemplate;@AutowiredprivateApplicationContextapplicationContext;@OverridepublicvoidaddUser(Stringname,Stringlocation){UserServiceuserService=applicationContext.getBean(UserService.class);userService.doAdd(名字);}@Override@TransactionalpublicvoiddoAdd(Stringname){Stringsql="insertintouser(`name`)values(?)";jdbcTemplate.update(sql,newObject[]{name});thrownewRuntimeException("保存用户失败");}}"3.设置@EnableAspectJAutoProxy(exposeProxy=true)如下,从AopContext中获取代理对象,然后调用方法"@ServicepublicclassUserServiceV5ImplimplementsUserService{@AutowiredprivateJdbcTemplatejdbcTemplate;@OverridepublicvoidaddUser(Stringname,Stringlocation){UserServiceuserService=(UserService)AopContext.currentProxy();userService.doAdd(名字);}@Override@TransactionalpublicvoiddoAdd(Stringname){Stringsql="insertintouser(`name`)values(?)";jdbcTemplate.update(sql,newObject[]{name});thrownewRuntimeException("保存用户失败");}}非public方法导致事务失败我们先来猜猜为什么非public方法会导致事务失败?“是不是因为非public方法不会生成代理对象?”我们在一个非public方法上加上@Transactional,调试到下面的代码,看是否会生成一个代理对象AbstractAutoProxyCreator#wrapIfNecessary"结论是不会生成代理对象,那为什么不生成代理对象呢?"应该不符合Pointcut的要求。我们已经提到交易对应的Pointcut是TransactionAttributeSourcePointcut。TransactionAttributeSourcePointcut#matchesmatches方法返回false,为什么会返回false?调试发现是以下代码引起的。AbstractFallbackTransactionAttributeSource#computeTransactionAttribute,即public方法可以正常生成代理对象,但是非public方法根本不会生成代理对象,因为不符合Pointcut的要求。异常类型不正确。默认只支持RuntimeException和Error,不支持异常检查。“为什么不支持异常检查?”取出我们上面分析的代码:当业务逻辑执行出现异常时,会调用TransactionAspectSupport#completeTransactionAfterThrowing方法。可以看到判断了异常类型,根据返回结果判断是否roll事务,调用下面的方法进行判断。RuleBasedTransactionAttribute#rollbackOn如果用户指定了回滚的异常类型,则根据用户指定的规则进行判断,否则使用默认规则。DefaultTransactionAttribute的默认规则是只支持RuntimeException和Error。我们可以通过@Transactional属性指定回滚的类型,一般是Exception。@Transactional(rollbackFor=Exception.class)