一、前言
在Web 开发中,Spring 框架已经成为了众多开发者的首选。Spring 的声明式事务管理是其中最重要的特性之一,它可以帮助我们简化业务逻辑的复杂度,并且确保在出现异常情况时数据的一致性。
事务失效情况很常见,但我们只要注意,就可以避免事情发生!在本文中,我将详细地介绍 Spring 声明式事务的源码实现
和事务失效常见的五种情况,并给出有效的解决方案。
其实我们常说的事务失效是声明式事务(@Transactional
)的失效,本文也是从声明式事务来进行演示的!
通过本文的学习,你将掌握如何正确地使用 Spring 的事务管理,减少生产事故。
一定要保持数据一致性
二、@Transactional注解参数解读
我们拿出几个经常使用的参数来简单介绍一下:
- propagation:指定事务的
传播行为
。其取值包括 REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER 和 NESTED 等。默认为REQUIRED
。
其中,REQUIRED 表示如果当前已经存在一个事务,则加入该事务,否则新建一个事务;而 REQUIRES_NEW 表示新建一个独立的事务,如果当前已经存在事务,则挂起当前事务。后面就不一一说了,大家可以自行百度哈! - isolation:指定事务的
隔离级别
。其取值包括 DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE 等。默认为 DEFAULT。
其中,DEFAULT 表示采用数据库的默认隔离级别. - timeout:指定事务的
超时时间
,单位为秒。默认为 -1,表示不设置超时时间。如果在规定时间内事务还未完成,则抛出 TransactionTimedOutException 异常。 - readOnly:指定事务
是否只读
,即是否允许修改数据。默认为 false,表示可以进行数据修改操作。如果将其设置为 true,则表示该事务仅能进行数据查询操作,不能进行数据修改操作,这样可以提高并发性能。 - rollbackFor:指定
哪些异常需要回滚
事务。其取值为一个 Class 数组,其中每个元素表示一个异常类型。默认为空,表示只有抛出 RuntimeException 或 Error 类型的异常时才回滚事务。 - noRollbackFor:指定
哪些异常不需要回滚
事务。其取值为一个 Class 数组,其中每个元素表示一个异常类型。默认为空,表示抛出任何异常都回滚事务。
三、声明式事务源码实现
声明式事务实现类为:TransactionInterceptor
,下面我们来一起看看这个类!
源码版本为Springboot2.7.1
public class TransactionInterceptor extends TransactionAspectSupport
implements MethodInterceptor, Serializable{}
TransactionInterceptor UML图:
声明式事务主要是通过AOP
实现,主要包括以下几个节点:
-
启动时扫描
@Transactional
注解:在启动时,Spring Boot会扫描所有使用了@Transactional注解的方法,并将其封装成TransactionAnnotationParser
对象。 -
AOP 来实现事务管理的核心类依然是
TransactionInterceptor
。TransactionInterceptor 是一个拦截器,用于拦截使用了 @Transactional 注解的方法 -
将TransactionInterceptor织入到目标方法中:在AOP编程中,使用
AspectJ
编写切面类,通过@Around
注解将TransactionInterceptor织入到目标方法中
。 -
在目标方法执行前创建事务:在目标方法执行前,TransactionInterceptor会调用
PlatformTransactionManager
创建一个新的事务,并将其纳入到当前线程的事务上下文中。 -
执行目标方法:在目标方法执行时,如果发生异常,则将事务状态标记为
ROLLBACK_ONLY
;否则,将事务状态标记为COMMIT
。 -
提交或回滚事务:在目标方法执行完成后,TransactionInterceptor会根据事务状态(COMMIT或ROLLBACK_ONLY)来决定是否提交或回滚事务。
源码:
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
@Override
@Nullable
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
@Override
public Object getTarget() {
return invocation.getThis();
}
@Override
public Object[] getArguments() {
return invocation.getArguments();
}
});
}
下面是核心处理方法,把不太重要的代码忽略了,留下每一步的节点。
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 获取事务属性
final TransactionManager tm = determineTransactionManager(txAttr);
// 准备事务
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
// 执行目标方法
Object retVal = invocation.proceedWithInvocation();
// 回滚事务
completeTransactionAfterThrowing(txInfo, ex);
// 提交事务
commitTransactionAfterReturning(txInfo);
}
如果还想看编程式事务实现的源码的可以看一下小编的这篇文章:
Spring的声明式事务和编程式事务源码、区别、优缺点、适用场景、实战
四、五种失效和解决方案
下面我们从几个情况来给大家展示失效场景并给出解决方案
1. 类没有被 Spring 管理
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional(rollbackFor = Exception.class)
public void addUser(User user) {
userDao.addUser(user);
}
}
如上代码所示,UserServiceImpl 类没有被声明为 Spring Bean,因此其中的 addUser() 方法无法受到 Spring 事务管理的保护。
我们使用Spring,要把类交给Spring进行管理,不然是无法生效!
解决方案: 交给spring进行管理bean,在类上添加:@Service
!
2. 方法不是public修饰
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional(rollbackFor = Exception.class)
protected void addUser(User user) {
userDao.addUser(user);
}
}
我们上面说了声明式事务是基于AOP
实现的,AOP是通过代理模式实现的,即为目标对象生成一个代理对象,当调用代理对象的方法时,会自动添加事务的控制代码。
在这种情况下,如果事务注释所在的方法不是public
的,则无法生成代理对象,因此事务代码将无法添加到方法执行前后,导致事务失效。
其实这种情况还是不经常这么使用,我们基本都是使用接口和实现大部分都是public修饰的!
解决方案: 使用public
来修饰方法
3. 异常被捕获并处理了
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional(rollbackFor = Exception.class)
public void addUser(User user) {
try {
userDao.addUser(user);
} catch (Exception e) {
// 处理异常,但没有抛出或重新抛出异常
log.error("add user error", e);
}
}
}
如上代码所示,如果 userDao.addUser() 方法抛出异常,但是在 UserServiceImpl.addUser() 中被捕获并处理了,事务检测不到有异常抛出,那么事务不会回滚。
解决方案: catch 处理完成后,在重新把异常在抛出去:throw e;
4. 同一个类中,方法内部调用
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser(User user) {
doAddUser(user);
}
@Transactional(rollbackFor = Exception.class)
public void doAddUser(User user) {
userDao.addUser(user);
}
}
Spring使用代理来实现事务控制,但是这种方法直接调用了this对象的方法
,则无法通过代理来拦截该方法调用,从而使得事务失效。
解决方案:
推荐使用有两种:
- 使用
ApplicationContext
来获取当前bean对象来调用doAddUser
方法 - 在
addUser
方法加上@Transactional(rollbackFor = Exception.class)
网上还有一些使用AopContext.currentProxy()
拿到代理对象的、自己注入自己的、抽到单独的bean里的
这里小编不是很推荐!
方法一完整展示:
如果觉得Service里注入ApplicationContext 不优雅,可以抽到单独的工具bean里!
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private ApplicationContext applicationContext;
@Override
public void addUser(User user) {
UserServiceImpl userService = applicationContext.getBean(UserServiceImpl.class);
userService.doAddUser(user);
}
@Transactional(rollbackFor = Exception.class)
public void doAddUser(User user) {
userDao.addUser(user);
}
}
5. MySQL存储引警不支持事务
MyISAM
存储引擎是 MySQL 的一种存储引擎,它是 MySQL 5.1 版本之前的默认存储引擎
,它是不支持事务的。从 MySQL 5.5 版本开始,InnoDB 成为了 MySQL 的默认存储引擎。我们想使用也可以切换到MyISAM引擎。
解决方案: 把mysql换到5.5以上使用InnoDB
存储引擎
补充使用MyISAM 方式:
-
表从 InnoDB 引擎转换为 MyISAM 引擎:
使用 ALTER TABLE 命令来更改表的引擎类型
ALTER TABLE table_name ENGINE = MyISAM;
- 默认的存储引擎设置为 MyISAM,
可以在 MySQL 配置文件中设置 default-storage-engine 参数。
default-storage-engine=MyISAM
- 创建表时指定MyISAM 引擎
要将表的引擎类型设置为 MyISAM,请在 CREATE TABLE 语句中包含 ENGINE = MyISAM 子句
CREATE TABLE table_name (
column1 datatype,
column2 datatype,
...
) ENGINE = MyISAM;
五、总结
本文总结了Spring 声明式事务的源码实现、五种常见的事务失效情况,并提供了相应的解决方案。
当然还有很多情况:被final修饰、多线程调用、传播行为使用不当、抛的异常不对应等等
理解 Spring 事务机制的,深入了解 Spring 事务的内部原理。同时,在使用声明式事务的过程中,我们也可以针对自己的业务场景进行定制化的配置,比如指定特定的事务传播机制、设置超时时间等,这些都有助于更好地应对复杂的业务场景和代码需求。这样才能真正地提高系统的可维护性、可扩展性和稳定性。
当然还有一些大事务,这样就需要编程式事务出厂了!
想了解编程式事务的可以看一下小编的这篇文章:
Spring/SpringBoot中的声明式事务和编程式事务源码、区别、优缺点、适用场景、实战
如果对你有帮助,还请动一下您的发财小手,关注一下公众号哈!!谢谢您的关注!!文章首发看!!!