事务考点
事务简介(四大原则)
事务(
TRANSACTION
)是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向
系统提交,要么都执行、要么都不执行. 事务是一个不可分割的工作逻辑单元.
事务具备四大原则:
- 原子性(Atomicity)
事务内的操作要么全部完成, 要么全都不执行成功.- 一致性(Consistency)
事务提交, 数据就要保持一致性.- 隔离性(Isolation)
事务间处理同一数据副本时, 保证数据的有效性.- 持久性(Durability)
一旦事务完成, 无论发生什么系统异常, 其结果都不受影响. 都需要被写入持久化存储器中.
Spring 事务管理
Spring 事务管理简介
Spring的事务管理支持编程式
也支持声明式
.
编程式: 需要在业务中显式调用事务的提交和回滚.
声明式: 通过AOP的方式支持声明式事务管理, 将事务管理的代码从业务中抽离出来, 通过动态代理注入进去.
事务传播属性
PROPAGATION_REQUIRED
如果当前没有事务, 就新建一个事务, 如果已经存在一个事务, 就加入到这个事务中.PROPAGATION_SUPPORTS
支持当前事务, 如果当前没有事务, 就以非事务的方式执行.PROPAGATION_MANDATORY
使用当前的事务, 如果当前没有事务, 就会抛出异常.PROPAGATION_REQUIRED_NEW
新建一个事务, 如果当前存在事务, 就把当前事务给挂起.PROPAGATION_NOT_SUPPORTED
以非事务的方式执行方法, 如果当前存在事务, 就把当前事务给挂起.PROPAGATION_NEVER
以非事务方式执行方法, 如果当前存在事务,就抛出异常.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("卢本伟伞兵一号准备就绪");
}
addUser2
和addUserException
不共用一个事务, 因此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
和外围方法
插入数据成功. 当然前提是, 你外围方法需要显式去捕获异常, 否则外围方法数据也会插入失败.
这里估计会觉得
NESTED
和REQUIRED_NEW
没什么区别… 嵌套事务和独立再起一个事务, 效果好像是一样的?
换个角度考虑就不一样了, 如果外部事务抛出异常,NESTED
会导致子事务也会回滚, 但是如果内部方法是REQUIRED_NEW
, 作为独立事务, 就不会受外部事务的影响了.
REQUIRED, REQUIRED_NEW, NESTED 传播级别总结
NESTED
和REQUIRED
修饰的内部方法都是属于外围方法的事务.
因此, 如果外围事务抛出异常, 这两种传播级别修饰的内部方法都会被回滚.
但是REQUIRED
是加入到当前的外围事务, 一旦内部方法抛出异常, 即使你外围方法加上捕获
语句, 也会导致其他子事务(REQUIRED
事务传播级别)和外围事务回滚.
而NESTED
修饰的内部方法, 是属于外围方法事务的一个子事务, 有对应的savePoint
, 所以只要外围方法加以捕获异常, 不会影响到外围事务回滚以及(其他REQUIRED
事务传播级别的子事务).NESTED
和REQUIRED_NEW
都可以实现内部事务回滚而不影响外围方法事务.
但是NESTED
因为是嵌套事务, 外围方法回滚的话, 作为外围方法事务的子事务也会被回滚.
然而REQUIRED_NEW
是通过开启独立的新事务实现的, 因此外部事务不会影响内部事务.