面试初体验:@Transactional引起的八股 + debug的拷打

发布于:2024-12-18 ⋅ 阅读:(7) ⋅ 点赞:(0)

问题提出

今天给大家分享我在最近一场面试中被问到还挺有意思的问题。面试开始,巴拉巴拉,比拉吧拉的输出,然后来到最后的手撕阶段,我以为会是算法题,结果 no! no ! no!面试官直接共享屏幕,粘贴出来下面这样一个代码:

@RequestMapping("/users")
@RestController
public class UsersController {
	// Bean的注入
    @Resource
    private UsersService usersService;
    
    @PostMapping("/add")
    public String addUsers(@RequestBody Users user) {
        this.updateUser(user);
        return  "ok";
    }

    @Transactional(rollbackFor = Exception.class)
    public String updateUser(Users user) {
        usersService.updateById(user);
        user.setId(null);
        int i = 1/0;
        return  usersService.addUsers(user) ? "添加成功" : "添加失败";
    }
}

【面试官说】:”给你几分钟,你看看代码运行会发生什么?“

【我心里活动】:我直接一眼就看到了@Transactional(rollbackFor = Exception.class)这个事务回滚的注解,同时还有 int i = 1/0;。心想,这么简单的问题,小case啦。然后直接跟面试官说,会执行到int i = 1/0时候会报错,一个运行时的异常错误,然后有关数据库操作直接根据@Transactional注解回滚了。

image-20241208160106586

【面试官】:你确定吗?

【我(自信ing)】:包的啦!

然后开始运行:

数据库开始数据:

image-20241208150752423

发送一个Post请求:

image-20241208151005678

看到postman中返回的报错500服务器错误,心里暗笑,哈哈哈手撕这么简单的吗。结果数据库一看:

image-20241208151123892

???啊?操作咋没有回滚??

image-20241208160031150

然后大脑开始飞速运转,将自己想到的@Transactional注解相关知识都过了一下:

事务回滚的误区

1. @Transactional的原理

@Transactional注解是通过Spring AOP(面向切面编程)实现的。它的核心作用是在方法执行时围绕方法进行事务的开启、提交或回滚。通常,事务会在以下几种情况下回滚:

  • 遇到RuntimeException(如ArithmeticException)。
  • 遇到Error(如OutOfMemoryError)。
  • 事务配置中指定的rollbackFor类型的异常。

在这个例子中,@Transactional(rollbackFor = Exception.class)指示Spring在遇到Exception及其子类时进行回滚。

2. AOP代理失效的问题

尽管代码中有@Transactional注解,但事务并没有生效。原因在于Spring的AOP机制。默认情况下,Spring AOP使用动态代理来增强标记为@Transactional的方法。这个代理会在方法执行前后插入事务控制逻辑。

但有一个问题,当你在同一个类内部调用带有@Transactional注解的方法时,Spring的AOP代理机制不会生效。这是因为,Spring AOP是基于代理的,它生成的代理对象是用于拦截方法调用的。而在同一个类中,调用this.updateUser(user)实际上是直接调用了updateUser方法,绕过了代理,从而导致事务管理失效。

3. 事务回滚失败的根本原因 √

事务回滚机制依赖于AOP代理,而Spring的AOP代理是基于接口或CGLIB的代理模式。当你在同一个类中直接调用事务方法时,Spring无法生成代理,因此事务管理逻辑没有被执行。具体而言:

  • AOP代理和方法调用:Spring会创建一个代理对象,并将所有的增强逻辑(如事务控制、权限校验等)附加到这个代理对象上。当外部调用事务方法时,代理对象会拦截并进行增强。
  • this调用的代理失效:在this.updateUser(user)中,this引用的是当前类的实例,而不是代理对象。因此,事务增强(即AOP切面)并没有生效,导致事务没有被正确管理。

然后急忙回答:哦哦哦,对对对,在同一个类中方法调用,会导致@Transactional失效

【面试官微微一笑(心想,八股文背的不错吗)】:那你知道是为什么导致的吗?

【我,心里暗喜,幸好有过了解】:其实这里就涉及到一个Spring中Bean生命周期的一个问题,生命周期如下:

xxx.class 
  --> 推断构造方法 
    --> 1. 实例化对象 (通过反射创建实例)
      --> 2. 依赖注入(赋值)
        --> 初始化前回调 (如实现了Aware接口的方法调用)
          --> 3. 初始化 (afterPropertiesSet() 或 @PostConstruct注解的方法)
            --> 初始化后(AOP增强)
              --> 创建代理对象 (如果需要AOP等增强)
                --> Bean放入单例池 Map<beanName, Bean对象>
                 --> 4.使用及之后的销毁

在一个Bean对象初始化后,会创建一个AOP的增强后的代理对象,同时将这个代理对象放入单例池中,在使用时候我们是通过

@Resource
private UsersService usersService;

这种注入的方式,拿去的是单例池中的AOP代理对象。如果是这样的调用自己类中的对象,其实是没有被AOP代理的,因此AOP并不会生效,同时@Transactional注解底层还是用AOP实现的,由于AOP失效了,导致这个事务注解也失效了。

    public String addUsers(@RequestBody Users user) {
        this.updateUser(user);
        return  "ok";
    }

解决思路

解决思路其实有两个:

1. 直接将该方法放到其他的对象中,然后注入调用

@RestController
@RequestMapping("/users")
public class UsersController {
    @Resource
    private UsersService usersService;

    @Resource
    private UserUpdateService userUpdateService;

    @PostMapping("/add")
    public String addUsers(@RequestBody Users user) {
        userUpdateService.updateUser(user);
        return "ok";
    }
}

@Service
public class UserUpdateService {
    @Resource
    private UsersService usersService;

    @Transactional(rollbackFor = Exception.class)
    public String updateUser(Users user) {
        usersService.updateById(user);
        user.setId(null);
        int i = 1 / 0; // Simulate an exception
        return usersService.addUsers(user) ? "添加成功" : "添加失败";
    }
}

2. AopContext.currentProxy()

使用AopContext.currentProxy()方法,它可以获取到当前的AOP代理对象。在UsersController类中,我们可以通过这个方法显式地调用带有@Transactional注解的方法,从而确保事务能够生效。

    @PostMapping("/add")
    public String addUsers(@RequestBody Users user) {
//        this.updateUser(user);
        UsersController proxy = (UsersController) AopContext.currentProxy();
        proxy.updateUser(user);
        return  "ok";
    }

3. 手动事务

尽管使用 @Transactional 注解可以简化事务管理,但在某些复杂的场景中,手动管理事务更具灵活性。手动事务管理允许我们精确控制事务的开始、提交和回滚,避免了某些事务管理注解的局限性,尤其是在涉及到多个步骤或复杂的业务逻辑时。以下是一个手动事务管理的示例:

// 开始事务
TransactionStatus status = transactionManager.getTransaction(definition);

try {
    // 执行传入的业务逻辑
    T result = executor.execute();
    // 提交事务
    transactionManager.commit(status);
    return result;
} catch (Exception e) {
    // 如果发生异常,回滚事务
    transactionManager.rollback(status);
    throw new RuntimeException("发生异常,事务回滚:" + e.getMessage(), e);
}

通过这种方式,我们可以在事务内执行复杂的业务逻辑,并确保在发生异常时回滚事务,从而保持数据的一致性和完整性。

4. 其他方法

除了上面两种方法,还有一些替代方案。例如,可以通过引入Spring的ApplicationContext来动态获取和调用带有事务管理的代理方法,但这相对比较复杂,一般不推荐使用。

总结

理解AOP的工作原理和Spring的事务管理机制对于开发高效、可靠的分布式应用至关重要。在日常开发中,尽可能使用手动事务管理可以避免事务失效的问题,并且能够更精确地控制事务的粒度,从而提高系统的性能和可维护性。同时,要注意在同一类中调用带有 @Transactional 注解的方法时,必须通过代理对象来调用,避免事务失效。