最近面试被问到的Spring事务(修订版)
前言
最近这段时间都在准备面试,所以好久没有推文。面试是一个自我查缺补漏的过程,这个过程还是得到了一定的收获。
面试了三家公司,都问到了事务,所以今天这篇文章就来总结关于事务这一块,面试官会怎么问,主要考察你对原理是否熟悉并且有足够的理解,下面我会结合一些案例来进行论述,干货满满,OK,进入正文。
ps:至于说一些概念和一些常识性的问题,比如,事务的四个特性和四个隔离级别,本文不会讲,没意义。
@Transactional注解原理
我们知道,只要我们开启了事务,在Service类的相关方法上加上@Transactional
注解,只要该类被IOC容器所管理,Spring底层就会扫描所有带有该注解的方法,在bean初始化前后做一些处理,最终实际上加入IOC容器的并不是该类本身,而是通过动态代理(Jdk动态代理或者cglib动态代理)生成的代理类,该代理类中所有带有 @Transactional
的方法是以环绕通知的形式在方法前后加入了几行代码。
如下图:
target
类addUser
方法加上了@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异常,翻译过来就是
不可检测的异常
,这种异常典型代表有RuntimeException
,NullPointerException
。
下面我就来通过实际的代码来证明上面的解释,多说无益,直接上代码,以下代码是通过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<Account, Integer> {}
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));
}
}
转账前表的记录如下:
如果执行过程没有发生异常,转账后的记录如下:
修改AccountServiceImpl
的transferAccount
方法,故意在方法中加载一个不存在的类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));
}
运行结果截图:
可以看到,发生java.lang.ClassNotFoundException
异常。查看数据库表,如下图,bingo和king的转账金额已经发生变化,事务并没有回滚,成功验证了上面的一个结论,checked异常抛出默认情况下是不会发生事务回滚的。
没错,Spring事务默认在RuntimeException异常或者Error下进行事务回滚,而RuntimeException异常就是unchecked异常。
但是我们可以在@Transactional
注解上通过rollbackFor
指定在什么异常下进行回滚,如下:
@Transactional(rollbackFor = ClassNotFoundException.class)
只要你像上面一样指定了回滚的异常类型,一旦发生该异常事务必然会回滚。笔者私下已经验证过了,这里不做具体演示。
接下来我们再次修改AccountServiceImpl
的transferAccount
方法,验证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);
}
saveUser
和saveUser2
两个方法再同一个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源码内部去探索吧。
尽管如此,希望这篇文章还是可以给你带来不一样的收获,谢谢一路以来的支持。