细说@Transactional用法及原理

==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍


第一天:
小白:小黑,今天发现数据库有好多脏数据,这咋回事?
小黑:(内心OS,什么玩意,都不自己查一下,直接扔个这么广泛的问题给我)呃。。。你要不去检查一下相关操作到数据库接口的逻辑?特别是插入和更新操作的
小白:嗯,有道理,我去check一下。
1个小时后……
小白:哎,小黑啊,我把相关接口check了一遍,发现正常情况下逻辑都没什么问题呀?
小黑:(内心OS,WTF?小白这个名字真不是盖的)呃。。。你都说了是正常情况下,有没有考虑在发生异常或者其他错误逻辑,导致的预期结果和行为和想象的不一样呢?建议你去针对各种情况debug一下,看看问题出在哪了
小白:啊,有道理,那我再去好好的测一下
3个小时后……
小白:嘿,小黑,我发现问题所在了
小黑:(内心OS,还不错,一点就通)咋回事?
小白:是这样的,有个新增资源的接口,大概逻辑是这样的

/*
假设1、2两步顺利完成,但是进行到3的时候抛异常了,并且没有进行任何后续的处理
那么就会导致新插入的这条资源是无用的,因为3报错了之后抛异常后,调用方也会接收到
它会认为本次add操作失败,返回给用户的提示就是XX创建失败,但是数据库已经存在了这条记录
所以造成脏数据。
*/
public int add1(){
     //1.业务处理1
     //2.将处理后的数据插入数据库
     //3.业务处理2
}

/*
这个就比较复杂,如果步骤3还调用了RPC接口,就涉及到业务补偿的问题
(这个后面有场景会说到,PS:这个不是小白说的,这是剧透)
*/
public int add2(){
     //1.业务处理1
     //2.将处理后的数据插入数据库
     //3.调用B服务的RPC接口
     //4.业务处理2
}

小黑:嗯,这样的话,确实会导致脏数据的产生,我建议你凡是涉及到insert和update的时候,加个事务,或者try…catch自己处理抛异常后的处理。
小白:嗯,我觉得用事务应该会比较方便,我去研究研究怎么用
小黑:因为我们用的是springboot开发项目,所以我推荐你去用@Transactional这个注解
小白:好的好的,我去试一下

第二天:
小黑:小白昨天那个问题怎么样了,解决了吗?
小白:嗯,已经解决了,我加了那个注解,上线了
小黑:嗯,线上观察、验证一下,看看还有没有脏数据
小白:欧了,绝对稳得一批
小黑:(内心OS,这话是第一百零几次说来着?)哈哈,OKOK
2个小时后……
小白:我Kao,怎么数据库里还有脏数据啊
小黑:(内心OS,我。。。)这不应该啊,如果事务生效了,就不会发生这种情况了吧,你昨天有验证事务是否生效吗?
小白:我。。。呃,哈哈,昨天我以为加上这个注解应该就稳了,就没去测试它。。。
小黑:(内心OS,不行了,必须得说说他)你这样。。。不行吧,基本的流程规范还是要有的吧,开发->自测->提测->ST回归->上线,这几个步骤你该不会是直接开发->上线了吧,偷懒也不能这样呀,不然以后因为某个重大bug影响了线上,那就凉了呀
小白:嗯嗯,我以后一定改!我现在马上回炉重造,这次保证彻底解决它!
小黑:(内心OS,看在他这么有决心的份上,帮帮他吧,但是也要让他学会自己查问题才行)嗯,有什么问题尽管来问我,我对@Transactional这个注解还是有一定了解的
小白:嗯嗯,太谢谢你了小黑,我现在马上去查问题
下班前1小时……
小白:哈,小黑,我找到问题原因啦
小黑:(内心OS,其实我大概想到了)嗯?什么原因?说说看?
小白:经过我的排查测试,现在已经修复好了,事务已经生效,肯定不会出现脏数据的问题了,现在我给你说一下排查的经过原来是这样子写的

//service层
@Service
class A{
    ……
    @Transactional
    public int add(){
            //1.业务处理1
            //2.数据库插入
            //3.业务处理2
    }  

    public int addResource(){
          //1.业务处理1
          //调用1
          this.add();
          //3.业务处理2
    }
    ……
}
//调用2
A a = new A();
a.add();
//调用2

我查了一下网上的文章,发现调用add方法的时候,需要用代理对象调用,不能用new 出来的对象或者this来调用,这会使事务不生效,然后我改成@Autowire注入。

//调用3
@Autowire
A a
a.add();

这下子事务就生效了!
小白:看,这样是不是没问题啦?
小黑:(内心OS,哎,还只是看到表面,没能深入去了解啊)嗯,这样改确实是没问题了,但是你知道为什么要这样吗?知道原理是什么吗?
小白:呃。。。这不是没时间去看嘛,要不趁现在你给我讲讲?
小黑:(内心OS,是时候展现我技术的时候了)嗯,让我来给你细细道来。首先,我先给你一个简单的例子看看

@Component
public class Run1 implements CommandLineRunner {

    @Autowired
    Run1 run;
    @Autowired
    TestRecord testRecord;

    @Record("run1->run")
    @Override
    public void run(String... args) throws Exception {
        //因为是直接用this来调用的,没走代理调用,@Record无效
        test();
        run.test();
        testRecord.test("A");
    }

    @Record("run1->test")
    public void test(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Component
    public class TestRecord {

        @Record("run1->TestRecord->test")
        public void test(String a) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Record {
        String value() default "";
    }

    @Aspect
    @Component
    public class RecordPointCut {
        @Pointcut("@annotation(com.example.demo.annotation.Run1.Record)")
        public void myAnnotationPointcut(){

        }

        @Before("myAnnotationPointcut()")
        public void before(JoinPoint joinPoint){
            System.out.println(joinPoint.getTarget() + " begin:" + System.currentTimeMillis());
        }

        @After("myAnnotationPointcut()")
        public void after(JoinPoint joinPoint){
            System.out.println(joinPoint.getTarget() + " end:" + System.currentTimeMillis());
        }
    }
}

小黑:在这个例子中,我写了一个非常简单的自定义注解,那他是如何生效的呢?
小白:这个。。。应该和@Transactional注解一样,使用代理对象调用就会生效
小黑:你还是只看到了表面,我再让你看看另一个例子。

@Component
public class Run2 implements CommandLineRunner{
    @Autowired
    private A a;
    @Autowired
    private B b;
    @Override
    public void run(String... args) throws Exception {
        a.test();
        b.test();
    }

    @Component
    class A {

        @NonNull
        public void test(){
            System.out.println("test");
        }
    }

    @Component
    class B{

        @Transactional
        public void test(){
            System.out.println("test");
        }
    }
}

小黑:你去debug这段代码,看看对象a和对象b有什么不一样?
5分钟后……
小白:咦,为什么对象a是原始对象,对象b是代理对象呢?小黑:嗯,抓到重点了,现在我们就来讨论为什么同样是@Autowire注入,但是一个是原始对象,一个是代理对象呢?
小白:嘿嘿,让我来猜一下,应该和那方法上的注解有关吧?
小黑:嗯,你猜对了,确实和那两个注解有关,有些注解,天生就会自带拦截器。
小白:拦截器?那是什么东西?
小黑:你可以简单理解为就是一个AOP,就像我上面写的那个自定义注解,就是用AOP实现的,功能是在调用test方法前后,进行一些额外的逻辑处理。
小白:嗯,AOP我知道,凡是被AOP切的类、方法都会生成一个代理对象,这个我理解,因为只有代理对象才能对原始的方法进行额外的扩展,类似这样

逻辑处理1
代理对象.test();
逻辑处理2

小黑:哈哈,不错嘛,说到重点了,就是这样的。所以知道为什么用到了@Transactional注解的类、方法会生成代理对象了吧?
小白:嗯嗯,大概了解了,让我稍微总结一下:

  • 凡是使用了拥有AOP功能的注解,@Autowire注入的时候,都会生成一个代理对象
  • 如果要使AOP功能的注解生效,那么调用该方法的对象必须是代理对象

小白:怎么样,总结得到位吧
小黑:嗯,还不错,但是现在你只是知其然,不知其所以然,想不想深入到源码去透彻理解一下@Transactional的工作原理?
小白:大佬!求之不得呀!
小黑:嗯,看你求知欲这么强,那我就带你看一下源码,了解一下它的工作原理吧,首先,从bean的实例化之前和初始化之后两个点说起。

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
	/**
	 * Convenience constant for subclasses: Return value for "do not proxy".
	 * @see #getAdvicesAndAdvisorsForBean
	 */
	@Nullable
	protected static final Object[] DO_NOT_PROXY = null;
	……
	@Override
	public Object postProcessBeforeInstantiation(Class beanClass, String beanName) {
		Object cacheKey = getCacheKey(beanClass, beanName);

		if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
			if (this.advisedBeans.containsKey(cacheKey)) {
				return null;
			}
			if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
				this.advisedBeans.put(cacheKey, Boolean.FALSE);
				return null;
			}
		}

		// Create proxy here if we have a custom TargetSource.
		// Suppresses unnecessary default instantiation of the target bean:
		// The TargetSource will handle target instances in a custom fashion.
		//这个说实话,还不知怎么用。。。
		TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
		if (targetSource != null) {
			if (StringUtils.hasLength(beanName)) {
				this.targetSourcedBeans.add(beanName);
			}
			Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
			Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		return null;
	}

	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyProxyReferences.remove(cacheKey) != bean) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
			return bean;
		}
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return bean;
		}

		// Create proxy if we have advice.
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}
}

小黑:好了,上面的代码片段只是方便复制,片段中很多代码也不需要去了解,因为没有用到,你硬要去理解它,过不了几天你还是会忘记的。首先,从这个类的名字,可以知道,主要功能是做自动代理的,就是强行扫描一波,看看有没有符合的bean,有的话就做成代理对象。接下来用上面那一个Run2类进行debug调试。PS:再次强调一下,这次讲源码的目的是为了了解@Transactional的工作原理,所以不要把自己定位到了解每一行代码。(内心OS,你想了解,哥也不懂啊,哈哈)
1.将断点下到postProcessBeforeInstantiation和postProcessAfterInitialization方法中,运行程序。其实不必看postProcessBeforeInstantiation这个方法了,因为我们没有使用CustomTargetSource这个东东,不会在实例化之前就创建代理的。所以直接断到postProcessAfterInitialization这个方法。
细说@Transactional用法及原理_第1张图片
对象a已经实例化和初始化完了,现在就看看它需不需要变成代理对象了,接着进去看看。
细说@Transactional用法及原理_第2张图片
这一步很重要,获取这个类中有没有使用到了拦截器,通俗点说就是有没有被切(AOP),如果有的话,那肯定得创建代理对象才行吧?
接着进入到方法内部,继续看是如何获取到拦截器的,不用进入太深
细说@Transactional用法及原理_第3张图片
进入到这里已经可以知道意图了,首先获取到所有的拦截器,然后看看这个类有没有用到里面的拦截器,OK,这个类A肯定没有用到,所以看到eligibleAdvisors是空,回到上一个方法。
细说@Transactional用法及原理_第4张图片
这里就返回不创建代理,再回到上个方法中
细说@Transactional用法及原理_第5张图片
因为返回的是DO_NOT_PROXY,所以不创建代理对象。
接着看看对象b
细说@Transactional用法及原理_第6张图片
因为类B中用到了@Transactional注解,它对应的有一个拦截器,所以找到了合适的拦截器。
细说@Transactional用法及原理_第7张图片
既然有拦截器,那么就要创建一个代理对象,并且返回这个代理对象而不是原始的对象。

接着我们去看方法调用的时候是怎样的
细说@Transactional用法及原理_第8张图片
可以看到,对象b确实变成了代理对象了,然后继续走,发现a.test是直接就进入到了方法内部,没有任何多余的处理。
细说@Transactional用法及原理_第9张图片
接着进入到b.test()的时候,会跳到CglibAopProxy的intercept方法

这个方法很重要,是干嘛的呢?它主要是拿到该目标方法所使用到的拦截器,可以有很多个,这里我们只用到了@Transactional对应的TransactionInterceptor拦截器。

拿到了之后又干嘛呢?我们接着往下走着看
细说@Transactional用法及原理_第10张图片
这一步是干嘛的呢? 它主要是构造一个cglib方法调用器,可以这么理解,你有N个拦截器,然后你把它交给cglib调用器,它来帮你顺序的处理它们。
细说@Transactional用法及原理_第11张图片
进入到proceed方法内部,第一次看一脸懵逼,但是经过我多次调试,明白它在干嘛了,来个伪代码让你看看

i = -1
void proceed()
while(i  < interceptors.size()){
	 //前置处理
	 ++i
	 proceed()
	 //后置处理
	 return;
}
//真正的方法调用

简单的说就是,递归的去处理这些拦截器,假设有3个拦截器,那么执行过程就是这样

拦截器1-前置处理
拦截器2-前置处理
拦截器3-前置处理
方法调用
拦截器3-后置处理
拦截器2-后置处理
拦截器1-后置处理

看完这个是不是豁然开朗了?
好,为了验证,我们加两个个拦截器,再运行到这一步

@Component
public class Run2 implements CommandLineRunner{
    @Autowired
    private A a;
    @Autowired
    private B b;
    @Override
    public void run(String... args) throws Exception {
        a.test();
        b.test();
    }

    @Component
    class A {

        @NonNull
        public void test(){
            System.out.println("test");
        }
    }

    @Component
    class B{

        @Transactional
        @Record1
        @Record2
        public void test(){
            System.out.println("test");
        }
    }

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Record1 {
        String value() default "";
    }

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Record2 {
        String value() default "";
    }

    @Aspect
    @Component
    public class RecordPointCut {
        @Pointcut("@annotation(com.example.demo.test.Run2.Record1)")
        public void myAnnotationPointcut1(){

        }

        @Pointcut("@annotation(com.example.demo.test.Run2.Record2)")
        public void myAnnotationPointcut2(){

        }

        @Around("myAnnotationPointcut1()")
        public void around1(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("Record1 before");
            joinPoint.proceed();
            System.out.println("Record1 after");
        }

        @Around("myAnnotationPointcut2()")
        public void around2(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("Record2 before");
            joinPoint.proceed();
            System.out.println("Record2 after");        }
    }
}

然后我们接着刚才那一步
细说@Transactional用法及原理_第12张图片
发现多了我们增加的两个拦截器
PS:第一个拦截器,网上说是将拦截器暴露出去巴拉巴拉的,我也是看不太懂为什么要这样,不知从何分析,所以这里就不讲这个拦截器了

我们接着走,第一个调用的是TransactionInterceptor拦截器
细说@Transactional用法及原理_第13张图片
调用这个拦截器的invoke方法,继续往里走
细说@Transactional用法及原理_第14张图片
再进入invokeWithinTransaction这个方法中
细说@Transactional用法及原理_第15张图片
和我写的自定义注解一样,分为三部分,前置处理,方法调用,后置处理三部分,一样的。

接着继续递归的调用proceed方法
细说@Transactional用法及原理_第16张图片
细说@Transactional用法及原理_第17张图片
细说@Transactional用法及原理_第18张图片
最后一个拦截器前置处理完成后,会直调用真实的方法
细说@Transactional用法及原理_第19张图片
细说@Transactional用法及原理_第20张图片
这里我们看到,最后真正调用test方法的,还是原始对象,相当于

代理对象.test(){
   //处理1
   原始对象.test()
   //处理2
}

最后就是递归回去,进行后置处理了
细说@Transactional用法及原理_第21张图片
最后的结果和预期的一样
细说@Transactional用法及原理_第22张图片
因为事务拦截器没有打印东西,所以没有显示出来。
小黑:怎么样,我讲了半天,你理解了吗?
小白:嗯嗯,讲得很清楚啊,工作原理我大致知道了,我给总结一下

  • 首先bean在初始化之后,动态代理会扫描一波,看看有没有需要代理的对象
  • 如果某个类中有用到拦截器,那么它的对象就判定为需要代理
  • 之后执行方法的时候,代理对象会先进行拦截器的前置处理,如果有N个拦截器,那么会顺序的进行各个拦截器的前置处理
  • 等所有拦截器的前置处理完成后,会使用原始对象调用它自己的方法
  • 最后代理对象会进行拦截器的后置处理,整个过程是通过递归完成的,所以顺序会是前1、前2、方法、后2、后1这样执行。

小黑:嗯,孺子可教也,看来是听进去了,以后知道咋用了吧(内心OS,嘿嘿,还有坑呢,先让你自己去踩一下,才会印象深刻),我建议你另起一个demo,运行一下我这段代码
小白:嗯嗯,木有问题,简单得很
10分钟后……
小白:啊,怎么会报错啊
小黑:那我怎么知道,现在也不早了,要下班了,你今晚回去好好看一下,我先溜了~
小白:呃。。。小黑这说溜就溜啊,不行,我得把这段代码运行起来

第三天:
小黑:嘿,小白你怎么这么大的黑眼圈
小白:就运行昨天你那个代码啊。。。你就给了一段代码,感觉也不完整,好像少了很多东西,狂报错。。。为什么在我们的项目直接就可以运行呢?
小黑:哈哈,因为我们都给你配好环境了,你当然可以直接在项目里面运行这段代码,真要你自己去弄一个demo,肯定会因为缺少某些jar包或者类而报错的
小白:嗯嗯,虽然昨天花的时间多了点,但是我把它给运行起来了!
小黑:哦?说说看,你都加了什么
小白:来,我给你列一下

  • AOP相关的

			org.springframework.boot
			spring-boot-starter-aop

  • JDBC相关的

			org.springframework.boot
			spring-boot-starter-jdbc

  • mysql相关的

			mysql
			mysql-connector-java
			runtime

  • 配置一个Datasource
@Component
public class DatasourceConfig {

    @Bean(value = "myDatasource")
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(url);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }
}

小白:做完这些,就可以正常运行了,特别是@Transactional注解,一开始我都不知道是在哪个jar包里,一查才知道,原来是在一个spring-tx的jar包里,而spring-tx又在spring-boot-starter-jdbc里。之后运行又报下面这个错误
细说@Transactional用法及原理_第23张图片
这个我还是看我们项目有配datasouce,然后有样学样的也配一个,结果还是报错。。。
细说@Transactional用法及原理_第24张图片
然后继续查,这个类是在哪个包下,然后添加mysql-connector-java这个后,终于能运行起来了!

小黑:哈哈,看来挺坎坷的呀,不过经过这一次,你应该对@Transactional注解比较熟悉了吧,并且类似的AOP注解,你应该都没问题了吧
小白:嗯嗯,虽然过程艰辛,但是收获颇多啊!
小黑:哈哈,继续加油,以后遇到问题要多思考为什么,不能简单的只看表面,这样再遇到一个类似的问题,你还是解决不了
小白:嗯嗯,学习了学习了~
小黑:好了,咱们好好搬砖吧~

PS:有些知识点没有深入讲有两个原因,第一个是没有必要深挖每一行代码,这样只会陷入到spring的深坑中,第二个就是,我对这些知识点掌握的不是很深,不敢对着代码直接就翻译出来,这句是干嘛的,那句是干嘛的,看的人懵逼,我也难受~,所以我还是坚持具体问题具体分析。

最后,中间有提到过一个题外话,关于A服务的方法中调用了B服务的接口,如果发生异常了,该怎么办? 篇幅有限,以及夜深了~,我简单提两句。这是实际项目中会发生的问题。

问题发生:

//1.业务处理1
//2.数据库插入
//3.B服务RPC调用
//4.业务处理2

假设运行到4抛出异常,事务回滚,数据库不存在脏数据
但是,B服务的接口已经调用完成,所以那边肯定会产生脏数据,怎么办?
第一个分布式事务,但是我没用过,因为项目不大,用不上这么高大上的。第二个,业务补偿,怎么做呢? 就是我这边抛异常后,我先catch,然后消息通知B服务,这一次的调用不算成功,请进行回滚操作,或者不用消息通知,直接调用B服务的delete接口,将这个脏数据清除掉。虽然low,但是能起作用,哈哈哈。

你可能感兴趣的:(springboot)