vlambda博客
学习文章列表

最近面试被问到的Spring事务(修订版)

前言

最近这段时间都在准备面试,所以好久没有推文。面试是一个自我查缺补漏的过程,这个过程还是得到了一定的收获。

面试了三家公司,都问到了事务,所以今天这篇文章就来总结关于事务这一块,面试官会怎么问,主要考察你对原理是否熟悉并且有足够的理解,下面我会结合一些案例来进行论述,干货满满,OK,进入正文。

ps:至于说一些概念和一些常识性的问题,比如,事务的四个特性和四个隔离级别,本文不会讲,没意义。

@Transactional注解原理

我们知道,只要我们开启了事务,在Service类的相关方法上加上@Transactional注解,只要该类被IOC容器所管理,Spring底层就会扫描所有带有该注解的方法,在bean初始化前后做一些处理,最终实际上加入IOC容器的并不是该类本身,而是通过动态代理(Jdk动态代理或者cglib动态代理)生成的代理类,该代理类中所有带有 @Transactional的方法是以环绕通知的形式在方法前后加入了几行代码。

如下图:

  • targetaddUser方法加上了@Transactional注解,所以我们可以看到proxy类的addUser方法上会被加上一些事务的操作。

  • 而没有加上事务注解的findAllUser方法,proxy类则是直接调用,并没有加上事务的操作。

问题一

了解了@Transactional的原理,下面就来说说我面试中被问到的问题:
一个没有事务注解的方法A调用了有事务注解的方法B,中途发生异常,会回滚吗?
如果你深刻领会了笔者上面的那个图就应该能回答这个问题了,proxy类直接调用target类的方法,相当于没有事务,发生异常自然不会回滚。

如果是一个有事务注解的方法A调用了没有事务注解的方法B,中途发生异常,那么必然会回滚。

如果是一个有事务注解的方法A调用了有事务注解的方法B,这种情况就是事务嵌套的情况,针对不同的事务传播机制,则需一一具体分析。

问题二

某个加了事务注解的方法发生异常却没有回滚,请解释原因?
发生异常却没有回滚,可能有几种情况:

情况一:

使用了try catch对运行时异常进行捕获,为什么是运行时异常呢,这里需要注意的一点,也是很多人会忽略的一点。

默认情况下,Spring会对unchecked异常进行事务回滚;如果是checked异常则不回滚。

简单解释下什么是unchecked异常checked异常

  • checked异常,翻译过来就是可检测的异常,这种异常需要我们在代码进行显示的try catch或者throws,如果你不进行try catch或者throws操作,编译不通过,这种异常典型代表有IOException

  • unchecked异常,翻译过来就是不可检测的异常,这种异常典型代表有RuntimeExceptionNullPointerException

下面我就来通过实际的代码来证明上面的解释,多说无益,直接上代码,以下代码是通过JPA操作数据库。
代码演示的是一个简单的转账操作。

  • 实体类Account对应数据库中的t_account

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "t_account")
public class Account {
    @Id
    @GeneratedValue
    private int account_id;

    private String name;

    private BigDecimal banlance;
}


  • AccountRepository

@Repository
public interface AccountRepository extends JpaRepository<AccountInteger{}


  • AccountService

public interface AccountService {

    void saveAccount(Account account);

    /**
     * 转账操作,此操作必须在同一事务下执行
     * @param from  from账户
     * @param to    to账户
     * @param money 转账金额
     */

    void transferAccount(Account from, Account to, BigDecimal money);
}


  • AccountServiceImpl

@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountRepository accountRepository;

    @Override
    public void saveAccount(Account account) {
        accountRepository.save(account);
    }

    @Override
    @Transactional
    public void transferAccount(Account from, Account to, BigDecimal money) {

        //from的账户余额减去money
        from.setBanlance(from.getBanlance().subtract(money));

        //to的账户余额加上money
        to.setBanlance(to.getBanlance().add(money));

        //执行保存操作
        this.saveAccount(from);
        this.saveAccount(to);
    }
}

代码是写在SpringBoot工程下的,至于SpringBoot依赖和配置就不详细说了,启动该SpringBoot工程,我们可以看到数据库生成了t_account表。

开始测试,先插入两条账户信息,然后进行转账操作,代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class TransactionApplicationTests {

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private AccountService accountService;

    @Test
    public void saveAccount() {
        Account bingo = new Account(1"bingo"new BigDecimal(1200));
        Account king = new Account(2"king"new BigDecimal(500));
        accountRepository.save(bingo);
        accountRepository.save(king);
    }

    //bingo给king转账200元
    @Test
    public void transferAccount() {
        Account bingo = new Account(1"bingo"new BigDecimal(1200));
        Account king = new Account(2"king"new BigDecimal(500));
        accountService.transferAccount(bingo,king,new BigDecimal(200));
    }
}

转账前表的记录如下:

最近面试被问到的Spring事务(修订版)

如果执行过程没有发生异常,转账后的记录如下:

最近面试被问到的Spring事务(修订版)

修改AccountServiceImpltransferAccount方法,故意在方法中加载一个不存在的类com.bingo.User,验证checked异常抛出是否会回滚。

@Override
@Transactional
public void transferAccount(Account from, Account to, BigDecimal money) throws ClassNotFoundException {

    //from的账户余额减去money
    from.setBanlance(from.getBanlance().subtract(money));

    //to的账户余额加上money
    to.setBanlance(to.getBanlance().add(money));

    //执行保存操作
    this.saveAccount(from);
    this.saveAccount(to);

    Class.forName("com.bingo.User");
}

再次运行测试方法transferAccount

@Test
public void transferAccount() throws ClassNotFoundException {
    Account bingo = new Account(1"bingo"new BigDecimal(1200));
    Account king = new Account(2"king"new BigDecimal(500));
    accountService.transferAccount(bingo,king,new BigDecimal(200));
}

运行结果截图:

最近面试被问到的Spring事务(修订版)

可以看到,发生java.lang.ClassNotFoundException异常。查看数据库表,如下图,bingo和king的转账金额已经发生变化,事务并没有回滚,成功验证了上面的一个结论,checked异常抛出默认情况下是不会发生事务回滚的。

最近面试被问到的Spring事务(修订版)

没错,Spring事务默认在RuntimeException异常或者Error下进行事务回滚,而RuntimeException异常就是unchecked异常。

但是我们可以在@Transactional注解上通过rollbackFor指定在什么异常下进行回滚,如下:

@Transactional(rollbackFor = ClassNotFoundException.class)

只要你像上面一样指定了回滚的异常类型,一旦发生该异常事务必然会回滚。笔者私下已经验证过了,这里不做具体演示。

接下来我们再次修改AccountServiceImpltransferAccount方法,验证unchecked异常抛出,事务必定会回滚。

@Override
@Transactional
public void transferAccount(Account from, Account to, BigDecimal money) {
    //from的账户余额减去money
    from.setBanlance(from.getBanlance().subtract(money));

    //to的账户余额加上money
    to.setBanlance(to.getBanlance().add(money));

    //执行保存操作
    this.saveAccount(from);
    this.saveAccount(to);

    int i = 10/0;
}

还是运行之前的transferAccount测试方法,运行结果截图:

查看数据库表,如下图,bingo和king的转账金额并没有发生变化,说明事务回滚,成功验证了上面的一个结论,unchecked异常抛出事务才会回滚。

但是一旦我们对unchecked异常进行try catch操作,事务将不会回滚,这里就不做详细描述了,笔者私底下已经验证过了。

情况二:

@Transactional注解只能应用到 public 可见度的方法上。如果应用在protected、private或者 package可见度的方法上,也不会报错,不过事务设置不会起作用。通常这种情况在jdk动态代理中一般不会发生,因为接口中的方法默认都是public修饰。

情况三:

事务嵌套的情况,该方法中内部调用了另一个带有事务注解的方法,外部方法与内部方法没有使用合适的事务传播机制,也会造成发生异常却没有全部回滚的情况,有可能是部分回滚。没有显示声明的话,一般Spring默认使用PROPAGATION_REQUIRED作为默认的事务传播机制。

简单举个例子,A,B两个方法都加上了事务注解,A方法内部调用了B方法,A使用默认的事务传播机制PROPAGATION_REQUIRED,而B使用PROPAGATION_REQUIRED_NEW,这种情况下,相当于A,B方法都有自己独立的事务,二者事务互不相干,无论A,B谁发生了异常,事务回滚互不影响。这种情况下,多个数据库操作并不具有原子性和一致性。

如果你对事务传播机制还不那么熟悉,下面表格可参考。
Spring事务的传播机制

传播行为 含义
PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行
PROPAGATION_MANDATORY 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务

彩蛋

上面我提到了一个问题:一个有事务注解的方法A调用了有事务注解的方法B,中途发生异常,会回滚吗?回滚情况是怎么样的?
上面没做具体分析,下面就此问题展开分析。

场景一:发生异常全部回滚

为了保障事务的原子性和一致性,一般我们要求嵌套事务发生异常,对数据库的所有操作都要进行回滚。
先看一段代码:

@Transactional
public void saveUser(User user,User user2) {
    userRepository.save(user); 
    saveUser2(user2);
}

@Transactional
public void saveUser2(User user) {
    userRepository.save(user);
}

saveUsersaveUser2两个方法再同一个Service类下,saveUser内部调用了saveUser2,乍一看,好像发生了事务嵌套对吧。如果是事务嵌套,一定会有事务的传播机制,这是毋庸置疑的吧,事务传播是Spring内部的实现机制。

我要告诉你的是,saveUser2事务并不生效,还是上面的事务代理原理的那张图,saveUser内部的操作是this.saveUser2(),并不会是proxy.saveUser2(),这种情况下相当于saveUser2的事务注解被注释了一样,根本不起作用。

有人说了,假如saveUser2事务没有失效,上面两个方法都使用Spring事务的默认传播机制,无论哪个方法发生异常,两个save到数据库的操作都会全部回滚。现在saveUser2事务失效了,无论哪个方法发生异常,两个save到数据库的操作也会全部回滚,都可以保证事务的原子性,结果并没有区别。

确实,这样调用是不会影响业务逻辑的,但是原理我们一定要懂,其实saveUser2内部是没有事务的,而有人以为异常回滚是因为事务传播机制在起作用,其实saveUser2压根就没有事务。为什么强调原理,不懂原理很容易就踩坑,看下面场景二。

场景二:发生异常部分回滚

在某些特定场景下,我们希望外部事务是不受内部事务的影响,即使内部事务发生异常回滚了,外部事务也能够正常提交。
对场景一的代码稍作修改,如下:

@Transactional
public void saveUser(User user,User user2) {
    userRepository.save(user);
    try{
        saveUser2(user2);
    }catch (Exception e){
        e.printStackTrace();
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser2(User user) {
    userRepository.save(user);
    int i = 1/0;
}

saveUser方法使用的是Spring默认的传播机制REQUIRED,saveUser2方法的事务传播机制是REQUIRES_NEW,这种场景下两个方法一旦发生事务嵌套,双方事务毫不相干,发生异常也是各自回滚。saveUser2方法内部有异常,saveUser对其进行try catch处理。

运行结果是:user和user2都成功插入数据库,saveUser2操作并没有回滚。
原因我们在场景一中已经解释了,再说一遍:saveUser内部的操作是this.saveUser2(),并不会是proxy.saveUser2(),这样就导致saveUser2方法事务失效。

如何让saveUser2方法的事务起作用呢?
想要让saveUser2方法的事务起作用,那么一定要获取该类在IOC容器中的代理类Proxy,调用方式是:Proxy.saveUser2()。这样saveUser2方法才会有事务,这样才有所谓的事务传播机制。代码如下:

@Transactional
public void saveUser(User user,User user2) {
    userRepository.save(user);
    try{
        //获取UserService的AOP代理类,通过代理类调用saveUser2方法才能有事务,否则直接调用saveUser2事务将失效
        UserService proxy = (UserService) AopContext.currentProxy();
        proxy.saveUser2(user2);
    }catch (Exception e){
        e.printStackTrace();
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUser2(User user) {
    userRepository.save(user);
    int i = 1/0;
}

这种情况下,运行结果就一定是user插入成功,而user2插入失败,因为user2的插入操作在saveUser2内部发生异常进行了回滚。

思考

推导了这么多,最后抛砖引玉,提出一个问题大家共同思考一下。

Spring内部是如何实现不同的事务传播机制的呢?

笔者百度了一下,没找到写的比较好的相关文章,要想搞懂这个问题,可能需要自己去深入Spring源码内部去探索吧。

尽管如此,希望这篇文章还是可以给你带来不一样的收获,谢谢一路以来的支持。