[Interview系列 知识储备回顾] Spring篇 - 事务

事务简介(四大原则)

事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向
系统提交,要么都执行、要么都不执行. 事务是一个不可分割的工作逻辑单元.
事务具备四大原则:

  1. 原子性(Atomicity)
    事务内的操作要么全部完成, 要么全都不执行成功.
  2. 一致性(Consistency)
    事务提交, 数据就要保持一致性.
  3. 隔离性(Isolation)
    事务间处理同一数据副本时, 保证数据的有效性.
  4. 持久性(Durability)
    一旦事务完成, 无论发生什么系统异常, 其结果都不受影响. 都需要被写入持久化存储器中.

Spring 事务管理

Spring 事务管理简介

Spring的事务管理支持编程式也支持声明式.

编程式: 需要在业务中显式调用事务的提交和回滚.

声明式: 通过AOP的方式支持声明式事务管理, 将事务管理的代码从业务中抽离出来, 通过动态代理注入进去.

事务传播属性

  1. PROPAGATION_REQUIRED 如果当前没有事务, 就新建一个事务, 如果已经存在一个事务, 就加入到这个事务中.
  2. PROPAGATION_SUPPORTS 支持当前事务, 如果当前没有事务, 就以非事务的方式执行.
  3. PROPAGATION_MANDATORY 使用当前的事务, 如果当前没有事务, 就会抛出异常.
  4. PROPAGATION_REQUIRED_NEW 新建一个事务, 如果当前存在事务, 就把当前事务给挂起.
  5. PROPAGATION_NOT_SUPPORTED 以非事务的方式执行方法, 如果当前存在事务, 就把当前事务给挂起.
  6. PROPAGATION_NEVER 以非事务方式执行方法, 如果当前存在事务,就抛出异常.
  7. PROPAGATION_NESTED 如果当前存在事务, 则在嵌套事务内执行, 如果当前没有事务, 则执行与PROPAGATION_REQUIRED类似的操作.

对于传播属性, 网上的资料可以说是写烂了.
笔者这里不打算一一讲解每个传播属性的示例, 而是针对比较常用的
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED_NEW
PROPAGATION_NESTED
以上三个事务传播属性作文章, 并从Spring 提供的TransactionManager源码作解读.

ps: 哦对了, 这里示例使用的是Mybatis这个ORM框架, 不过不重要, Mybatis本身使用的还是Spring提供的事务管理.
在这里插入图片描述
这里偷懒就直接用spring-boot构建项目了, 毕竟(auto-configure)开箱即用还是很香的.
但是笔者说真的不是很建议过于依赖注解, 这样会对于理解框架没有本质上的帮助…
笔者还是比较喜欢xml的配置, 首先诸多项目在交付过程中, 很多配置还是需要exclude到外部做调整.
咳咳, 扯远了, 开始正题…

启动类, 这里设置的系统属性, 后续是为了了解cglib生成的代理类内部实现的, 不过实际上对于事务传播的理解, 不需要在这里深入, 没有这方面的需求.

在这里插入图片描述
在这里插入图片描述

这里本是需要手动注入DataSourceTransactionManager这个事务管理类.

在这里插入图片描述

org.springframework.boot.autoconfigure.jdbc. DataSourceTransactionManagerAutoConfiguration中自动注入该事务管理类, 前提是@ConditionalOnClass({JdbcTemplate.class, PlatformTransactionManager.class})

事务传播 [传播的本质]

@Transactional(
        propagation = Propagation.REQUIRED
)
public void addUser(User user) {
    userMapper.insert(User.builder().name("outer").build());
    addUser2();
    addUserException(user);
}

@Transactional(
        propagation = Propagation.REQUIRED
)
public void addUser2(){
    userMapper.insert(User.builder().name("bofa").build());
}

@Transactional(
        propagation = Propagation.REQUIRED
)
public void addUserException(User user){
    userMapper.insert(user);
    throw new RuntimeException("卢本伟伞兵一号准备就绪");
}

这里说明一点, 事务是通过aop动态代理(默认都是cglib, 况且jdk的动态代理只作用于接口), 我们都能理解, 但是如果作为同一个代理类内的方法, 不存在外围和内围的划分, 即使#addUser内部调用的#addUser2和#addUserException加上不同的事务传播级别, 也都是在同一个事务内执行. 多说无益, 运行下程序康康.

在这里插入图片描述
跟进执行被代理的类的方法, 典型的cglib代理方法
在这里插入图片描述

到service对象对应的外围方法(这里说的不够严谨… 不能说是外围方法)
这里可以看到#addUser2是在该代理类中继续执行的, 而不是在另一个事务中去让另一个代理类去执行.
在这里插入图片描述
因此, 很明显的, 这里会抛出定义好的异常.
在这里插入图片描述
并在TransactionAspectSupport#invokeWithinTransaction方法中, 捕获该异常, 去判断是否需要回滚.
在这里插入图片描述

这里得以证实, #addUser2#addUserException是在与执行#addUser时, 同一个代理类中执行, 因此就算你设置事务传播级别为REQUIRES_NEW, 新建一个新的事务, 并将当前事务挂起也没用.
事务的传播, 是存在于不同的代理类所需要织入的代理方法.

事务传播 REQUIRED

Propagation.REQUIRED 如果当前没有事务, 就新建一个事务, 如果已经存在一个事务, 就加入到这个事务中.

外围无事务
-------------- userOuterService -------------------
public void addUser(User user) {
    userMapper.insert(User.builder().name("outer").build());
    userInnerService.addUser2();
    userInnerService.addUserException(user);
}
-------------- userInnerService -------------------
@Transactional(propagation = Propagation.REQUIRED)
public void addUser2(){
    userMapper.insert(User.builder().name("bofa").build());
}

@Transactional(propagation = Propagation.REQUIRED)
public void addUserException(User user){
    userMapper.insert(user);
    throw new RuntimeException("卢本伟伞兵一号准备就绪");
}

addUser2addUserException不共用一个事务, 因此addUser2数据插入成功, addUserException抛出异常, 外围方法由于没有事务, 外围方法插入的数据并不回滚.

在这里插入图片描述

外围有事务
-------------- userOuterService -------------------
@Transactional(propagation = Propagation.REQUIRED)
public void addUser(User user) {
    userMapper.insert(User.builder().name("outer").build());
    userInnerService.addUser2();
    userInnerService.addUserException(user);
}
-------------- userInnerService -------------------
@Transactional(propagation = Propagation.REQUIRED)
public void addUser2(){
    userMapper.insert(User.builder().name("bofa").build());
}

@Transactional(propagation = Propagation.REQUIRED)
public void addUserException(User user){
    userMapper.insert(user);
    throw new RuntimeException("卢本伟伞兵一号准备就绪");
}

由于外围方法#addUser有事务, #addUser2和#addUserException会加入到外围方法的事务中.
因此, addUserException抛出异常, 外围事务回滚, 数据均未插入成功.

这里我们加上try-catch, 去尝试是否能捕获内围方法抛出的异常

-------------- userOuterService -------------------
@Transactional(propagation = Propagation.REQUIRED)
public void addUser(User user) {
    userMapper.insert(User.builder().name("outer").build());
    userInnerService.addUser2();
    try{
        userInnerService.addUserException(user);
    }catch (Exception ignore){
    }
}

这里会发现很有趣的现象, 数据还是被回滚了, 均未插入, 明明加了try-catch为啥还是这个情况??
Debug跟进TransactionAspectSupport类, 发现调用addUserException这个方法时, 事务抛出异常被捕获, 并在completeTransactionAfterThrowing中, 去尝试回滚.
根据判定结果, 由于事务未设置savePoint以及并不是独立的事务, 因此只是设置了rollBackOnly的标志.
在这里插入图片描述
在这里插入图片描述
之后才去catch捕获异常, 但是这时候rollbackOnly的标志已经打上了, 因此无奈事务回滚.
在这里插入图片描述
这里我们也能看到, 异常的确被捕获了, 并没有执行addUser代理的completeTrasactionAfterThrowing
在这里插入图片描述
在最后, 去判断是否需要回滚, 根据标志来判定.
在这里插入图片描述
在这里插入图片描述

图加的多了, 篇幅也就乱了… 很多点还是需要独立跟进debug去了解的, 文章并不能大程度上解惑.

事务传播 REQUIRED_NEW

考虑到篇幅原因, 我这边就不再对内部实现做详解了. 直接上例子、结果和总结.

PROPAGATION_REQUIRED_NEW 新建一个事务, 如果当前存在事务, 就把当前事务给挂起.

-------------- userOuterService -------------------
@Transactional(propagation = Propagation.REQUIRED)
public void addUser(User user) {
    userMapper.insert(User.builder().name("outer").build());
    userInnerService.addUser2();
    try{
        userInnerService.addUserException(user);
    } catch (Exception ignore){
    }
}
--- userInnerService -------------------
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addUser2(){
    userMapper.insert(User.builder().name("bofa").build());
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addUserException(User user){
    userMapper.insert(user);
    throw new RuntimeException("卢本伟伞兵一号准备就绪");
}

内部方法改为REQUIRED_NEW, 也就是内部方法分别起独立事务执行.
其结果, 显而易见的, addUser2外围方法插入数据成功. 当然前提是, 你外围方法需要显式去捕获异常, 否则外围方法数据也会插入失败.

事务传播 REQUIRED_NESTED

PROPAGATION_NESTED 如果当前存在事务, 则在嵌套事务内执行, 如果当前没有事务, 则执行与PROPAGATION_REQUIRED类似的操作.

-------------- userOuterService -------------------
@Transactional(propagation = Propagation.REQUIRED)
public void addUser(User user) {
    userMapper.insert(User.builder().name("outer").build());
    userInnerService.addUser2();
    try{
        userInnerService.addUserException(user);
    }catch (Exception ignore){

    }
}
--- userInnerService -------------------
@Transactional(propagation = Propagation.NESTED)
public void addUser2(){
    userMapper.insert(User.builder().name("bofa").build());
}

@Transactional(propagation = Propagation.NESTED)
public void addUserException(User user){
    userMapper.insert(user);
    throw new RuntimeException("卢本伟伞兵一号准备就绪");
}

内部方法改为NESTED, 也就是内部方法会嵌套在外围方法的事务中去执行.
其结果, 显而易见的, addUser2外围方法插入数据成功. 当然前提是, 你外围方法需要显式去捕获异常, 否则外围方法数据也会插入失败.

这里估计会觉得NESTEDREQUIRED_NEW没什么区别… 嵌套事务和独立再起一个事务, 效果好像是一样的?
换个角度考虑就不一样了, 如果外部事务抛出异常, NESTED会导致子事务也会回滚, 但是如果内部方法是REQUIRED_NEW, 作为独立事务, 就不会受外部事务的影响了.

REQUIRED, REQUIRED_NEW, NESTED 传播级别总结

  • NESTEDREQUIRED修饰的内部方法都是属于外围方法的事务.
    因此, 如果外围事务抛出异常, 这两种传播级别修饰的内部方法都会被回滚.
    但是REQUIRED是加入到当前的外围事务, 一旦内部方法抛出异常, 即使你外围方法加上捕获语句, 也会导致其他子事务(REQUIRED事务传播级别)和外围事务回滚.
    NESTED修饰的内部方法, 是属于外围方法事务的一个子事务, 有对应的savePoint, 所以只要外围方法加以捕获异常, 不会影响到外围事务回滚以及(其他REQUIRED事务传播级别的子事务).
  • NESTEDREQUIRED_NEW都可以实现内部事务回滚而不影响外围方法事务.
    但是NESTED因为是嵌套事务, 外围方法回滚的话, 作为外围方法事务的子事务也会被回滚.
    然而REQUIRED_NEW是通过开启独立的新事务实现的, 因此外部事务不会影响内部事务.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值