一个@Transaction哪里来这么多坑?
共 7334字,需浏览 15分钟
·
2020-08-28 13:56
前言
在之前的文章中已经对Spring中的事务做了详细的分析了,这篇文章我们来聊一聊平常工作时使用事务可能出现的一些问题(本文主要针对使用@Transactional
进行事务管理的方式进行讨论)以及对应的解决方案
事务失效
事务回滚相关问题
读写分离跟事务结合使用时的问题
事务失效
事务失效我们一般要从两个方面排查问题
数据库层面
数据库层面,数据库使用的存储引擎是否支持事务?默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表特地修改了存储引擎,例如,你通过下面的语句修改了表使用的存储引擎为MyISAM
,而MyISAM
又是不支持事务的
alter table table_name engine=myisam;
这样就会出现“事务失效”的问题了
「解决方案」:修改存储引擎为Innodb
。
业务代码层面
业务层面的代码是否有问题,这就有很多种可能了
我们要使用Spring的声明式事务,那么需要执行事务的Bean是否已经交由了Spring管理?在代码中的体现就是类上是否有
@Service
、Component
等一系列注解
「解决方案」:将Bean交由Spring进行管理(添加@Service
注解)
@Transactional
注解是否被放在了合适的位置。在上篇文章中我们对Spring中事务失效的原理做了详细的分析,其中也分析了Spring内部是如何解析@Transactional
注解的,我们稍微回顾下代码:
❝代码位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
中❞
也就是说,默认情况下你无法使用@Transactional
对一个非public的方法进行事务管理
「解决方案」:修改需要事务管理的方法为public
。
出现了自调用。什么是自调用呢?我们看个例子
@Service
public class DmzService {
public void saveAB(A a, B b) {
saveA(a);
saveB(b);
}
@Transactional
public void saveA(A a) {
dao.saveA(a);
}
@Transactional
public void saveB(B b){
dao.saveB(a);
}
}
上面三个方法都在同一个类DmzService
中,其中saveAB
方法中调用了本类中的saveA
跟saveB
方法,这就是自调用。在上面的例子中saveA
跟saveB
上的事务会失效
那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP
的,当容器在创建dmzService
这个Bean时,发现这个类中存在了被@Transactional
标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类
public class DmzServiceProxy {
private DmzService dmzService;
public DmzServiceProxy(DmzService dmzService) {
this.dmzService = dmzService;
}
public void saveAB(A a, B b) {
dmzService.saveAB(a, b);
}
public void saveA(A a) {
try {
// 开启事务
startTransaction();
dmzService.saveA(a);
} catch (Exception e) {
// 出现异常回滚事务
rollbackTransaction();
}
// 提交事务
commitTransaction();
}
public void saveB(B b) {
try {
// 开启事务
startTransaction();
dmzService.saveB(b);
} catch (Exception e) {
// 出现异常回滚事务
rollbackTransaction();
}
// 提交事务
commitTransaction();
}
}
上面是一段伪代码,通过startTransaction
、rollbackTransaction
、commitTransaction
这三个方法模拟代理类实现的逻辑。因为目标类DmzService
中的saveA
跟saveB
方法上存在@Transactional
注解,所以会对这两个方法进行拦截并嵌入事务管理的逻辑,同时saveAB
方法上没有@Transactional
,相当于代理类直接调用了目标类中的方法。
我们会发现当通过代理类调用saveAB
时整个方法的调用链如下:
实际上我们在调用saveA
跟saveB
时调用的是目标类中的方法,这种清空下,事务当然会失效。
常见的自调用导致的事务失效还有一个例子,如下:
@Service
public class DmzService {
@Transactional
public void save(A a, B b) {
saveB(b);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}
当我们调用save
方法时,我们预期的执行流程是这样的
也就是说两个事务之间互不干扰,每个事务都有自己的开启、回滚、提交操作。
但根据之前的分析我们知道,实际上在调用saveB方法时,是直接调用的目标类中的saveB方法,在saveB方法前后并不会有事务的开启或者提交、回滚等操作,实际的流程是下面这样的
由于saveB方法实际上是由dmzService也就是目标类自己调用的,所以在saveB方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因:「自调用时,调用的是目标类中的方法而不是代理类中的方法」
「解决方案」:
自己注入自己,然后显示的调用,例如: @Service
public class DmzService {
// 自己注入自己
@Autowired
DmzService dmzService;
@Transactional
public void save(A a, B b) {
dmzService.saveB(b);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}这种方案看起来不是很优雅 利用 AopContext
,如下:@Service
public class DmzService {
@Transactional
public void save(A a, B b) {
((DmzService) AopContext.currentProxy()).saveB(b);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}❝使用上面这种解决方案需要注意的是,需要在配置类上新增一个配置 // exposeProxy=true代表将代理类放入到线程上下文中,默认是false
@EnableAspectJAutoProxy(exposeProxy = true)❞ 个人比较喜欢的是第二种方式
总结
事务回滚相关问题
想回滚的时候事务却提交了 想提交的时候被标记成只能回滚了(rollback only)
rollbackFor
属性不够了解导致的。unchecked
异常(继承自 RuntimeException
的异常)或者 Error
才回滚事务;其他异常不会触发回滚事务,已经执行的SQL会提交掉。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor
属性。❞TransactionAspectSupport#completeTransactionAfterThrowing
方法中❞RuntimeException
或者Error
才会回滚public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
RuntimeException
或者Error
时也回滚,请指定回滚时的异常,例如:@Transactional(rollbackFor = Exception.class)
Transaction rolled back because it has been marked as rollback-only
@Service
public class DmzService {
@Autowired
IndexService indexService;
@Transactional
public void testRollbackOnly() {
try {
indexService.a();
} catch (ClassNotFoundException e) {
System.out.println("catch");
}
}
}
@Service
public class IndexService {
@Transactional(rollbackFor = Exception.class)
public void a() throws ClassNotFoundException{
// ......
throw new ClassNotFoundException();
}
}
DmzService
的testRollbackOnly
方法跟IndexService
的a
方法都开启了事务,并且事务的传播级别为required
,所以当我们在testRollbackOnly
中调用IndexService
的a
方法时这两个方法应当是共用的一个事务。按照这种思路,虽然IndexService
的a
方法抛出了异常,但是我们在testRollbackOnly
将异常捕获了,那么这个事务应该是可以正常提交的,为什么会抛出异常呢?AbstractPlatformTransactionManager
中❞内部事务发生异常,外部事务catch异常后,内部事务自行回滚,不影响外部事务
// @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void a() throws ClassNotFoundException{
// ......
throw new ClassNotFoundException();
}
requires_new
时,两个事务完全没有联系,各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。但是传播级别为nested
时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,实际上是回滚到保存点上,并且当外部事务提交时,内部事务才会提交,外部事务如果回滚,内部事务会跟着回滚。内部事务发生异常时,外部事务catch异常后,内外两个事务都回滚,但是方法不抛出异常
@Transactional
public void testRollbackOnly() {
try {
indexService.a();
} catch (ClassNotFoundException e) {
// 加上这句代码
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
}
}
RollbackOnly
。这样当提交事务时会进入下面这段代码读写分离跟事务结合使用时的问题
配置多数据源 依赖中间件,如 MyCat
MyCat
等中间件那么需要注意:「只要开启了事务,事务内的SQL都会使用写节点(依赖于具体中间件的实现,也有可能会允许使用读节点,具体策略需要自行跟DB团队确认)」@Transactional
注解中的readOnly
属性就应该要慎用。我们使用readOnly
的原本目的是为了将事务标记为只读,这样当MySQL服务端检测到是一个只读事务后就可以做优化,少分配一些资源(例如:只读事务不需要回滚,所以不需要分配undo log段)。但是当配置了读写分离后,可能会可能会导致只读事务内所有的SQL都被路由到了主库,读写分离也就失去了意义。总结
SpringMVC
。整个来说相比于Spring源码,我觉得应该不算特别难。有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️