@Transactional 事务注解失效


概述

在使用Spring框架时,可以有两种使用事务的方式,一种是编程式的,一种是声明式的,@Transactional 注解就是声明式的。

首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些方便程序员更加容易操作事务的方式。

在一个方法加上了 @Transactional注解之后,Spring会基于这个类生成代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果方法上存在 @Transactional 注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑就会将事务提交;反之,当业务逻辑方法发生异常,则会将事务回滚。

当然,针对哪些异常回滚事务也是可以配置的,可以通过 @Transactional注解中的rollbackFor属性进行指定,默认情况下会对 RuntimeException和Error 进行回滚。

示例

针对下文用例,定义:
数据库表:

CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` VARCHAR(16) NOT NULL COMMENT 'name',
  `age` INT NOT NULL COMMENT 'age',
  PRIMARY KEY(`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'

实体类:

public class User{
	private Long id;
	private String name;
	private Integer age;
    // 略get/set方法
}

事务注解失效原因列举

  1. @Transactional 注解标注的方法修饰符为非public时,@Transactional 注解将不会生效

    例如以下代码:定义一个 @Transactional 注解实现,修饰一个默认访问修饰符的方法

    @Service
    public class UserService {
    
        @Autowired
        private UserMapper mapper;
    
        @Transactional
         void defaultMethodInsert() {
            int count = mapper.insert(new User(1L,"Tom", 19));
            if (count > 0) {
                throw new RuntimeException("exception intercept");
            }
            mapper.insert(new User(2L,"Mary", 18));
        }
    }
    

    测试用例:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {CustomerApplication.class})
    public class TransactionFailureTest {
    
        @Autowired
        private UserService userService;
    
        @Test
        public void testInvoke() {
            userService.methodInsert();
        }
    }
    

    输出结果:
    在这里插入图片描述测试用例结果显示抛出异常,但数据库表写入 Tom。事务没有开启,因此在方法抛出异常时,Tom的写入没有进行回滚;如果InvokcationService#invokeInsertTestWrongModifier方法的访问修饰符改为public的话,将会正常开启事务,Tom和Mary的写入会同时进行回滚。

  2. @Transactional 注解标注的方法被final或static修饰

    事务是通过AOP实现,即通过代理的方式完成,将一个方法定义成final意味着方法不能被重写,那么事务就失效了。
    我们将 UserService#defaultMethodInsert的修饰符改成final,同样测试出当异常抛出,Tom的写入没有被回滚。
    注意:如果某个方法是static时,意味着该方法属于类本身,同样无法通过动态代理,变成事务方法。

  3. 在类内部调用类内部@Transactional标注的方法;

    业务逻辑方法新增UserService#testInnerInvoke调用类内部的事务方法UserService#methodInsert

    @Service
    public class UserService {
    
         @Autowired
         private UserMapper mapper;
    
         @Transactional
         public void methodInsert() {
            int count = mapper.insert(new User(1L,"Tom", 19));
            if (count > 0) {
                throw new RuntimeException("exception intercept");
            }
            mapper.insert(new User(2L,"Mary", 18));
        }
    
        public void testInnerInvoke() {
            this.methodInsert();
        }
    }
    

    测试用例:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {CustomerApplication.class})
    public class TransactionFailureTest {
    
        @Autowired
        private UserService userService;
    
        @Test
        public void methodInnerInvoke() {
            System.out.println("methodInnerInvoke >>>");
            userService.testInnerInvoke();
        }
    
        @Test
        public void methodOuterInvoke() {
            System.out.println("methodOuterInvoke >>>");
            userService.methodInsert();
        }
    }
    

    输出结果:
    在这里插入图片描述执行测试用例中TransactionFailureTest#methodInnerInvoke方法测试方法内部调用@Transactional,Tom的写入事务操作同样不会进行回滚,即事务已失效。

    在这里插入图片描述执行测试用例中TransactionFailureTest#methodOuterInvoke方法测试,Tom和Mary的写入事务都已进行回滚,即事务生效;

  4. 未被Spring管理

    在开发过程中容易忽略一些细节问题,比如忘记了@Controller、@Service、@Component、@Repository等注解。
    没使用以上注解交给spring进行管理,那么事务就不会生效。
    应用@Transactional的类必须是Spring管理的bean,如果该类没有被Spring容器管理,则事务不会生效。

  5. 数据库引擎不支持事务

    mysql5之前,默认的数据库引擎是MYISAM,它的好处是:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。
    但是数据库引擎(如MyISAM)不支持事务,此时@Transactional注解将不会生效。

  6. 异常处理不当

    如果在事务方法中捕获了异常但是没有抛出,或者抛出了检查型异常(即继承自Exception而非RuntimeException),那么事务可能不会回滚。
    在业务逻辑方法内修改UserService#methodInsert,捕捉异常后只打印文字;

    @Service
    public class UserService {
    
         @Autowired
         private UserMapper mapper;
    
         @Transactional
         public void methodInsert() {
             try {
                 int count = mapper.insert(new User(1L,"Tom", 19));
                 if (count > 0) {
                     throw new RuntimeException("exception intercept");
                 }
                 mapper.insert(new User(2L,"Mary", 18));
             } catch (Exception e) {
                 System.out.println("catch exception >>>");
             }
        }
    }
    

    测试用例:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {CustomerApplication.class})
    public class TransactionFailureTest {
    	@Test
        public void methodCatchExInvoke() {
        	System.out.println("methodCatchExInvoke >>>");
        	userService.methodInsert();
        }
    }
    

    输出结果: 在这里插入图片描述

    执行测试用例方法,结果显示Tom写入事务操作未回滚,即事务失效。

    当然,@Transactional注解的rollbackFor属性可以指定异常类型进行回滚,当方法抛出的异常非rollbackFor指定的类型,事务操作同样不会进行回滚。

  7. 数据源未配置事务管理器

    Spring事务需要使用支持事务的DataSource并配置PlatformTransactionManager,如果未正确配置,则事务无法生效。

  8. 传播行为不匹配

    事务的传播行为(propagation behavior)需要匹配调用栈,如果配置不当,可能会导致事务意外提交或回滚。
    其实,在使用@Transactional注解时,是可以指定propagation参数的。
    该参数得作用是指定事务的传播特性,spring目前支持7中传播特性:
    REQUIRED;SUPPORTS;MANDATORY;REQUIRES_NEW;NOT_SUPPORTED;NEVER;NESTED
    如果事务设置成Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则抛出异常。

  9. AOP切面顺序问题

    如果项目中存在多个AOP切面,并且它们的顺序配置不当,可能会导致事务切面未能正确执行,从而影响事务的生效。

  10. 注解标注位置问题

    如果将@Transactional注解标注在接口方法上,并且使用CGLIB作为代理方式,则无法解析到该注解,导致事务失效。通常建议将注解标注在接口的实现类方法上。

总结

总之为了避免@Transactional注解失效,需要确保方法的访问权限正确、没有被final或static修饰、正确配置Spring容器和数据库支持事务、正确处理异常、以及正确配置AOP切面顺序等。同时,在编写代码时,应尽量避免在同一个类中通过this调用事务方法,而是应该通过Spring容器或其他方式调用。

  • 33
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值