在现代软件开发中,事务处理是必不可少的一部分。当多个操作需要作为一个整体来执行时,事务
可以确保数据的完整性和一致性
,并避免出现异常和错误情况。在SpringBoot
框架中,我们可以使用声明式事务和编程式事务
来管理事务处理。其中事务的坑也是不少,比较常见的就是事务失效,大家可以看看!
这篇博客将重点探讨这两种事务处理方式的源码实现、区别、优缺点、适用场景以及实战。
我们来接着说事务,里面还涉及到三个知识点,大家可以自行百度好好了解!
事务的特性
事务的传播行为
隔离级别
本篇文章主要讲的就是实现事务的两种方式的分析!
让我们开始探索声明式事务和编程式事务吧!
文章很长,耐心看完希望对你有帮助!
本文源码是使用:springboot2.7.1
我们在启动类上添加注解:@EnableTransactionManagement
后续使用就可以添加注解@Transactional(rollbackFor = Exception.class)
使用,或者是使用编程式事务使用了 !
后面我们在详细演示怎么使用哈!
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);
}
编程式事务主要下面的代码:
public class TransactionTemplate extends DefaultTransactionDefinition
implements TransactionOperations, InitializingBean{}
TransactionTemplate UML图:
TransactionTemplate
类的execute()
方法封装了事务的具体实现,通过调用TransactionCallback对象的doInTransaction()
方法来执行业务逻辑并管理事务
。在具体实现中,TransactionTemplate类会自动控制事务的提交和回滚,并将异常抛出给上层调用者进行处理。
@Override
@Nullable
public T execute(TransactionCallback action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
上面说了源码里的大体实现,下面我们来介绍一下两者区别:
技术实现方式:声明式事务是通过AOP技术来实现的,而编程式事务是通过编写具体的代码来实现的。
代码耦合度:声明式事务可以将事务处理逻辑从业务代码中分离出来,从而降低代码的耦合度。而编程式事务需要在业务代码中显式地调用事务管理代码,因此会增加代码的耦合度。
难易程度:声明式事务相对来说比较容易上手,开发人员只需要学习注解或XML配置即可。而编程式事务需要开发人员理解事务管理的底层机制,并编写具体的代码。
性能影响:由于声明式事务是由容器来处理的,所以在一些场景下可能会对性能产生影响,大事务会有很多问题(下面在说一下大事务出现的问题)。而编程式事务由于直接调用事务管理API,相对来说会有更好的性能表现。
总体而言,声明式事务和编程式事务都有各自的优缺点,开发人员需要根据具体需求选择适合的方式来控制事务。
补充:
大事务时间过长可能会导致以下问题:
数据库锁定
:当事务涉及到大量的数据操作时,事务可能会占用数据库资源并长时间锁定相关数据。这可能会导致其他事务无法访问或修改这些数据,从而降低系统的并发性能和吞吐量。资源耗尽
:长时间运行的事务需要占用更多的系统资源,如内存和CPU等。如果系统资源不足,可能会导致系统出现延迟、死锁等问题,甚至导致系统崩溃。事务失败概率增加
:当事务时间过长时,事务执行期间可能会发生各种错误,如网络故障、硬件故障、操作系统问题等。此时,事务可能无法成功提交,导致数据丢失或数据不一致。应用程序超时
:应用程序通常会为每个事务设置一个超时时间,以避免事务持续时间过长。如果事务持续时间超过设定的超时时间,则应用程序可能会因为等待事务完成而阻塞,最终导致应用程序崩溃或超时。回滚时间增加
:如果事务失败需要回滚,长时间运行的事务将需要更长的时间来进行回滚操作。这可能会导致数据不一致或丢失,并增加数据库维护的工作量。
因此,开发人员应该尽量避免事务时间过长,合理地设置事务范围、优化事务操作方式以及减少数据访问次数等措施,以提高系统的并发性能和吞吐量。
方案:
大事务可以拆分小的事务,一下查询方面的可以提取出来,操作数据库的抽离出来专门加上事务。
也可以使用CompletableFuture
组合式异步编排来解决大事务的问题!!
声明式事务通常通过AOP
技术实现,在方法或类级别上声明事务属性。
声明式事务的优点包括:
简化代码
:开发人员只需要关注业务逻辑,而无需手动管理事务,可以减少代码复杂度和工作量。可配置性强
:事务属性可以通过XML文件、注解等方式进行配置,灵活方便。易于扩展
:可以通过AOP技术轻松地扩展使其支持新的事务策略。
声明式事务存在以下缺点:
限制较大
:事务属性需要在方法或类级别进行声明,这可能会导致某些情况下难以满足特定的业务需求。难以调试
:由于事务是在AOP层面进行管理的,因此在调试时可能难以追踪事务管理的具体细节。
编程式事务通常通过API
接口实现,开发人员可以在代码中显式地管理事务。
编程式事务的优点包括:
灵活性强
:开发人员可以在代码中根据具体业务需要来控制事务的具体范围和属性。易于调试
:由于事务管理在代码层面上实现,因此开发人员可以很容易地追踪事务管理的细节。
编程式事务存在以下缺点:
代码复杂度高
:需要在代码中手动处理事务,并处理各种异常情况,可能会增加代码的复杂度和工作量。可配置性差
:事务的范围和属性需要在代码中显式声明,这可能会导致一些特定的业务需求难以满足。
总之,声明式事务和编程式事务各有优缺点。开发人员需要根据具体业务需求和场景选择使用合适的事务管理方式。
声明式事务通常适用于以下场景:
大型企业级应用程序,需要管理多个事务。
代码结构比较复杂,使用声明式事务可以更好地管理和维护代码(大事务参考上方的方案)。
声明式事务可以将事务管理与业务逻辑分离,从而使得应用程序更加松耦合。
编程式事务通常适用于以下场景:
需要更精确地控制事务的范围和处理逻辑。
编程式事务通常比声明式事务更加灵活,可以根据业务逻辑的需要来自定义事务的范围、隔离级别以及回滚机制等。
在某些高并发场景下,可以使用编程式事务仅针对需要操作的数据进行锁定,而不是对整个业务逻辑加事务。
在实际场景中,可以根据需求综合考虑使用声明式事务和编程式事务的优势来进行选择。
根据不同的用户量来具体选择,在几乎没有并发量的系统设计一条异步编排反而大材小用,可能造成资源的浪费;但是有需要等待远程API的响应时
,使用异步编排可以将等待时间最小化
,并使得应用程序不必阻塞等待API响应,从而提高用户体验。
很多事情没有绝对化,只有相对化,只要能支持现有正常的使用,不管什么样的设计都是没问题的! 可能好的设计会使系统在经受并发量增大的过程中无感,还是要调研清楚,从而设计出更好的方案,防止资源浪费!
尽管小编还没有什么架构经验,但还是对架构充满兴趣,不想做架构师的开发不是好开发哈!!当然你也可以走管理!!
这里就简单模拟一下,为了模拟报错,把OperIp设置为唯一!
@Transactional(rollbackFor = Exception.class)
大家经常使用,就不多演示了!
@Transactional(rollbackFor = Exception.class)
@Override
public void template() {
SysLog sysLog = new SysLog();
sysLog.setOperIp("123");
SysLog sysLog1 = new SysLog();
sysLog1.setOperIp("hhh");
log.info("插入第一条数据开始========");
testMapper.insert(sysLog);
log.info("插入第一条数据完成========");
log.info("插入第二条数据开始========");
testMapper.insert(sysLog);
log.info("插入第二条数据完成========");
}
此时数据没有数据,全部回滚成功!
首先注入TransactionTemplate
:
@Autowired
private TransactionTemplate transactionTemplate;
后面直接使用即可:
@Override
public void template() {
SysLog sysLog = new SysLog();
sysLog.setOperIp("123");
SysLog sysLog1 = new SysLog();
sysLog1.setOperIp("hhh");
log.info("插入第一条数据开始========");
testMapper.insert(sysLog);
log.info("插入第一条数据完成========");
transactionTemplate.execute(status -> {
log.info("编程式事务中:插入第一条数据开始========");
testMapper.insert(sysLog1);
log.info("编程式事务中:插入第一条数据完成========");
log.info("编程式事务中:插入第二条数据开始========");
int insert = testMapper.insert(sysLog);
log.info("编程式事务中:插入第二条数据完成========");
return insert;
});
}
查看数据库,第一条不在编程式事务内不会参与回滚!
本文介绍了SpringBoot框架中的声明式事务和编程式事务,并分析了它们的源码实现、区别、优缺点、适用场景以及实战。
无论是采用哪种方式来管理事务,都需要考虑到业务需求和开发团队的实际情况,选择合适的事务处理方式,以确保系统的可靠性和稳定性。
希望通过本文的介绍,你能够更好地理解声明式事务和编程式事务的概念和原理,在开发过程中选择合适的事务处理方式,提高项目的可维护性和稳定性。