五分钟教你手写 SpringBoot 本地事务管理实现
白菜Java自习室涵盖核心知识
1.SpringBoot事务
一直在用SpringBoot中的@Transactional来做事务管理,但是很少没想过SpringBoot是如何实现事务管理的,今天从源码入手,看看@Transactional是如何实现事务的,最后我们结合源码的理解,自己动手写一个类似的注解来实现事务管理,帮助我们加深理解。
1.1.事务的隔离级别
事务为什么需要隔离级别呢?这是因为在并发事务情况下,如果没有隔离级别会导致如下问题:
- 脏读(DirtyRead):当A事务对数据进行修改,但是这种修改还没有提交到数据库中,B事务同时在访问这个数据,由于没有隔离,B获取的数据有可能被A事务回滚,这就导致了数据不一致的问题。
- 丢失修改(LostToModify):当A事务访问数据100,并且修改为100-1=99,同时B事务读取数据也是100,修改数据100-1=99,最终两个事务的修改结果为99,但是实际是98。事务A修改的数据被丢失了。
- 不可重复读(UnrepeatableRead):指A事务在读取数据X=100的时候,B事务把数据X=100修改为X=200,这个时候A事务第二次读取数据X的时候,发现X=200了,导致了在整个A事务期间,两次读取数据X不一致了,这就是不可重复读。
- 幻读(PhantomRead):幻读和不可重复读类似。幻读表现在,当A事务读取表数据时候,只有3条数据,这个时候B事务插入了2条数据,当A事务再次读取的时候,发现有5条记录了,平白无故多了2条记录,就像幻觉一样。
不可重复读VS幻读
不可重复读的重点是修改:同样的条件,你读取过的数据,再次读取出来发现值不一样了,重点在更新操作。
幻读的重点在于新增或者删除:同样的条件,第1次和第2次读出来的记录数不一样,重点在增删操作。
所以,为了避免上述的问题,事务中就有了隔离级别的概念,在Spring中定义了五种表示隔离级别的常量TransactionDefinition:
- ISOLATION_DEFAULT:数据库默认的隔离级别,MySQL默认采用的REPEATABLE_READ隔离级别。
- ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL中通过MVCC解决了该隔离级别下出现幻读的可能。
- ISOLATION_SERIALIZABLE:串行化隔离级别,该级别可以防止脏读、不可重复读以及幻读,但是串行化会影响性能。
1.2.Spring中事务的传播机制
为什么Spring中要搞一套事务的传播机制呢?这是Spring给我们提供的事务增强工具,主要是解决方法之间调用,事务如何处理的问题。比如有方法A、方法B和方法C,在A中调用了方法B和方法C。伪代码如下:
MethodA(){
MethodB();
MethodC();
}
假设三个方法中都开启了自己的事务,那么他们之间是什么关系呢?MethodA的回滚会影响MethodB和MethodC吗?Spring中的事务传播机制就是解决这个问题的。
Spring中定义了七种事务传播行为:
- PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
- PROPAGATION_SUPPORTS:如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
- PROPAGATION_MANDATORY:如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
- PROPAGATION_REQUIRES_NEW:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
- PROPAGATION_NOT_SUPPORTED:总是非事务地执行,并挂起任何存在的事务。
- PROPAGATION_NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常。
- PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按TransactionDefinition.PROPAGATION_REQUIRED属性执行。
1.3.Spring中事务如何实现异常回滚的
回顾完了事务的相关知识,接下来我们正式来研究下SpringBoot中如何通过@Transactional来管理事务的,我们重点看看它是如何实现回滚的。
在Spring中TransactionInterceptor和PlatformTransactionManager这两个类是整个事务模块的核心,我们重点研究下这两个类的源码。
- TransactionInterceptor负责拦截方法执行,进行判断是否需要提交或者回滚事务。
- PlatformTransactionManager是Spring中的事务管理接口,真正定义了事务如何回滚和提交。
TransactionInterceptor类中的代码有很多,我简化一下逻辑,方便说明:
//以下代码省略部分内容
publicObjectinvoke(MethodInvocationinvocation)throwsThrowable{
//获取事务调用的目标方法
Class>targetClass=(invocation.getThis()!=null?AopUtils.getTargetClass(invocation.getThis()):null);
//执行带事务调用
returninvokeWithinTransaction(invocation.getMethod(),targetClass,invocation::proceed);
}
invokeWithinTransaction简化逻辑如下:
//以下代码省略部分内容
protectedObjectinvokeWithinTransaction(Methodmethod,@NullableClass>targetClass,finalInvocationCallbackinvocation)throwsThrowable{
ObjectretVal;
try{
//调用真正的方法体
retVal=invocation.proceedWithInvocation();
}
catch(Throwableex){
//如果出现异常,执行事务异常处理
completeTransactionAfterThrowing(txInfo,ex);
throwex;
}
finally{
//最后做一下清理工作,主要是缓存和状态等
cleanupTransactionInfo(txInfo);
}
//如果没有异常,直接提交事务
commitTransactionAfterReturning(txInfo);
returnretVal;
}
事务出现异常回滚的逻辑completeTransactionAfterThrowing如下:
//以下代码省略部分内容
protectedvoidcompleteTransactionAfterThrowing(@NullableTransactionInfotxInfo,Throwableex){
//判断是否需要回滚,判断的逻辑就是看有没有声明事务属性,同时判断是不是在目前的这个异常中执行回滚
if(txInfo.transactionAttribute!=null&&txInfo.transactionAttribute.rollbackOn(ex)){
//执行回滚
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
else{
//否则不需要回滚,直接提交即可
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
上面的代码已经把Spring的事务的基本原理说清楚了,如何进行判断执行事务,如何回滚。下面到了真正执行回滚逻辑的代码中PlatformTransactionManager接口的子类,我们以JDBC的事务为例,DataSourceTransactionManager就是jdbc的事务管理类。跟踪上面的代码rollback(txInfo.getTransactionStatus())可以发现最终执行的代码如下:
@Override
protectedvoiddoRollback(DefaultTransactionStatusstatus){
DataSourceTransactionObjecttxObject=(DataSourceTransactionObject)status.getTransaction();
Connectioncon=txObject.getConnectionHolder().getConnection();
if(status.isDebug()){
logger.debug("RollingbackJDBCtransactiononConnection["+con+"]");
}
try{
//调用jdbc的rollback进行回滚事务
con.rollback();
}
catch(SQLExceptionex){
thrownewTransactionSystemException("CouldnotrollbackJDBCtransaction",ex);
}
}
这里小结下Spring中事务的实现思路,Spring主要依靠TransactionInterceptor来拦截执行方法体,判断是否开启事务,然后执行事务方法体,方法体中catch住异常,接着判断是否需要回滚,如果需要回滚就委托真正的TransactionManager比如JDBC中的DataSourceTransactionManager来执行回滚逻辑。提交事务也是同样的道理。
这里用个流程图展示下思路:
2.手写注解实现事务回滚
我们弄清楚了Spring的事务执行流程,那我们可以模仿着自己写一个注解,实现遇到指定异常就回滚的功能。这里持久层就以最简单的JDBC为例。我们先梳理下需求,首先注解我们可以基于Spring的AOP来实现,接着既然是JDBC,那么我们需要一个类来帮我们管理连接,用来判断异常是否回滚或者提交。
2.1.Maven加入依赖
org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-jdbc
2.2.新建一个注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public@interfaceMyTransaction{
//指定异常回滚
Class[]rollbackFor()default{};
}
2.3.新建连接管理器
该类帮助我们管理连接,该类的核心功能是把取出的连接对象绑定到线程上,方便在AOP处理中取出,进行提交或者回滚操作。
@Component
publicclassDataSourceConnectHolder{
@Autowired
privateDataSourcedataSource;
/**
*线程绑定对象
*/
ThreadLocalresources=newNamedThreadLocal<>("Transactionalresources");
publicConnectiongetConnection(){
Connectioncon=resources.get();
if(con!=null){
returncon;
}
try{
con=dataSource.getConnection();
//为了体现事务,全部设置为手动提交事务
con.setAutoCommit(false);
}catch(SQLExceptione){
e.printStackTrace();
}
resources.set(con);
returncon;
}
publicvoidcleanHolder(){
Connectioncon=resources.get();
if(con!=null){
try{
con.close();
}catch(SQLExceptione){
e.printStackTrace();
}
}
resources.remove();
}
}
2.4.新建一个切面
这部分是事务处理的核心,先获取注解上的异常类,然后捕获住执行的异常,判断异常是不是注解上的异常或者其子类,如果是就回滚,否则就提交。
@Aspect
@Component
publicclassMyTransactionAopHandler{
@Autowired
privateDataSourceConnectHolderconnectHolder;
Class[]es;
//拦截所有MyTransaction注解的方法
@org.aspectj.lang.annotation.Pointcut("@annotation(你的包路径.MyTransaction)")
publicvoidTransaction(){
}
@Around("Transaction()")
publicObjectTransactionProceed(ProceedingJoinPointproceed)throwsThrowable{
Objectresult=null;
Signaturesignature=proceed.getSignature();
MethodSignaturemethodSignature=(MethodSignature)signature;
Methodmethod=methodSignature.getMethod();
if(method==null){
returnresult;
}
MyTransactiontransaction=method.getAnnotation(MyTransaction.class);
if(transaction!=null){
es=transaction.rollbackFor();
}
try{
result=proceed.proceed();
}catch(Throwablethrowable){
//异常处理
completeTransactionAfterThrowing(throwable);
throwthrowable;
}
//直接提交
doCommit();
returnresult;
}
/**
*执行回滚,最后关闭连接和清理线程绑定
*/
privatevoiddoRollBack(){
try{
connectHolder.getConnection().rollback();
}catch(SQLExceptione){
e.printStackTrace();
}finally{
connectHolder.cleanHolder();
}
}
/**
*执行提交,最后关闭连接和清理线程绑定
*/
privatevoiddoCommit(){
try{
connectHolder.getConnection().commit();
}catch(SQLExceptione){
e.printStackTrace();
}finally{
connectHolder.cleanHolder();
}
}
/**
*异常处理,捕获的异常是目标异常或者其子类,就进行回滚,否则就提交事务。
*/
privatevoidcompleteTransactionAfterThrowing(Throwablethrowable){
if(es!=null&&es.length>0){
for(Classe:es){
if(e.isAssignableFrom(throwable.getClass())){
doRollBack();
}
}
}
doCommit();
}
}
2.4.编写一个Service
saveTest方法调用了2个插入语句,同时声明了@MyTransaction事务注解,遇到Exception就进行回滚。
@Service
publicclassMyTransactionTest{
@Autowired
privateDataSourceConnectHolderholder;
//一个事务中执行两个sql插入
@MyTransaction(rollbackFor=NullPointerException.class)
publicvoidsaveTest(intid){
save(id,"白菜Java自习室");
save(id+10,"白菜Java自习室");
thrownewRuntimeException();
}
//执行sql
privatevoidsave(intid,Stringvalue){
Stringsql="insertintotestvalues(?,?)";
Connectionconnection=holder.getConnection();
PreparedStatementstmt=null;
try{
stmt=connection.prepareStatement(sql);
stmt.setInt(1,id);
stmt.setString(2,value);
stmt.executeUpdate();
}catch(SQLExceptione){
e.printStackTrace();
}
}
}
我们自己通过JDBC结合Spring的AOP自己写了个@MyTransactional的注解,实现了遇到指定异常回滚的功能。
到此这篇关于五分钟教你手写SpringBoot本地事务管理实现的文章就介绍到这了,更多相关SpringBoot本地事务管理内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。