Java千万级数据插入开启事务_Spring 源码分析:不得不重视的 Transaction 事务

关注Java极客技术,后台回复“java”,获取Java知识体系/面试必看资料

12c7a5e923e6512439c97e78a5206e64.png

7a8e08df72cfd2acf02736c46fa005c3.png

吐血推荐

今天,正式介绍一下Java极客技术知识星球

SpringBoot 精髓之 SpringBoot-starter

Spring 源码学习(八) AOP 使用和实现原理

Java:前程似锦的 NIO 2.0

我们谈谈面试技巧(初入职场年轻人该学的)

7a8e08df72cfd2acf02736c46fa005c3.png

业务系统的数据,一般最后都会落入到数据库中,例如 MySQLOracle 等主流数据库,不可避免的,在数据更新时,有可能会遇到错误,这时需要将之前的数据更新操作撤回,避免错误数据。

Spring 的声明式事务能帮我们处理回滚操作,让我们不需要去关注数据库底层的事务操作,可以不用在出现异常情况下,在 try / catch / finaly 中手写回滚操作。

Spring 的事务保证程度比行业中其它技术(例如 TCC / 2PC / 3PC 等)稍弱一些,但使用 Spring 事务已经满足大部分场景,所以它的使用和配置规则也是值得学习的。

接下来一起学习 Spring 事务是如何使用以及实现原理吧。

使用例子

1.创建数据库表

create 

2.创建对应数据库表的 PO

public 

3.创建表与实体间的映射

在使用 JdbcTemplate 时很纠结,在 Java 类中写了很多硬编码 SQL,与 MyBatis 使用方法不一样,为了示例简单,使用了 JdbcTemplate,不过还是建议朋友们用 MyBatis,让代码风格整洁。

public 

4.创建数据操作接口

public 

5.创建数据操作接口实现类

跟书中例子不一样,没有在接口上加入事务注解,而是在公共方法上进行添加,可以在每个方法上自定义传播事件、隔离级别。

public 

6.创建配置文件

<?xml  version="1.0" encoding="UTF-8"?>

7.添加依赖

记得添加数据库连接和 jdbctx 这两个 spring 模块的依赖

optional(project(":spring-jdbc"))  // for Quartz support
optional(project(":spring-tx"))  // for Quartz support
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.6'

8.启动代码

public 

通过上面的代码,我做了两个测试:

  1. 配置文件中,没开启事务。 也就是 这一行被注释了,虽然我们执行的方法中抛出了 RuntimeExcepton,但是数据库中依旧被插入了数据。

  2. 配置文件中,开启事务。 将上面的注释去掉,删掉数据库中的记录,重新执行启动代码,发现数据没有被插入, 在程序抛出异常情况下,Spring 成功执行了事务,回滚了插入操作。


注解属性 @Transactional

具体位置在:org.springframework.transaction.annotation.Transactional

属性类型作用
valueString可选的限定描述符,指定使用的事务管理器
propagation枚举:Propagation可选的事务传播行为
isolation枚举:Isolation可选的事务隔离级别设置
readOnlyboolean设置读写或只读事务,默认是只读
rollbackForClass 数组,必须继承自 Throwable导致事务回滚的异常类数组
rollbackForClassName类名称数组,必须继承自 Throwable
noRollbackForClass 数组,必须继承自 Throwable不会导致事务回滚的异常类数组
noRollbackForClassName类名称数组,必须继承自 Throwable

事务的传播性 Propagation

  • REQUIRED

这是默认的传播属性,如果外部调用方有事务,将会加入到事务,没有的话新建一个。

  • PROPAGATION_SUPPORTS

如果当前存在事务,则加入到该事务;如果当前没有事务,则以非事务的方式继续运行。

  • PROPAGATION_NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  • PROPAGATION_NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。


事务的隔离性 Isolation
  • READ_UNCOMMITTED

最低级别,只能保证不读取
物理上损害的数据,允许脏读

  • READ_COMMITTED

只能读到已经提交的数据

  • REPEATABLE_READ

可重复读

  • SERIALIZABLE

串行化读,读写相互阻塞

这里只是简单描述了一下这两个主要属性,因为底层与数据库相关,可以看下我之前整理过的 MySQL锁机制


Spring 中实现逻辑

介绍完如何使用还有关键属性设定,本着知其然,知其所以然的学习精神,来了解代码是如何实现的吧。


解析

之前在解析自定义标签时提到,AOPTX 都使用了自定义标签,按照我们上一篇 AOP 的学习,再来一遍解析自定义标签的套路:事务自定义标签。

定位到 TxNamespaceHandler 类的初始化方法:

@Override

根据上面的方法,Spring 在初始化时候,如果遇到诸如 开头的配置后,将会使用 AnnotationDrivenBeanDefinitionParser 解析器的 parse 方法进行解析。

public BeanDefinition parse(Element element, ParserContext parserContext) {

Spring 中的事务默认是以 AOP 为基础,如果需要使用 AspectJ 的方式进行事务切入,需要在 mode 属性中配置:

<tx:annotation-driven mode="aspectj"/>

本篇笔记主要围绕着默认实现方式,动态 AOP 来学习,如果对于 AspectJ 实现感兴趣请查阅更多资料~


注册 InfrastructureAdvisorAutoProxyCreator

AOP 一样,在解析时,会创建一个自动创建代理器,在事务 TX 模块中,使用的是 InfrastructureAdvisorAutoProxyCreator

首先来看,在默认配置情况下,AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext) 做了什么操作:

private 

在这里注册了代理类和三个 bean,这三个关键 bean 支撑了整个事务功能,为了待会更好的理解这三者的关联关系,我们先来回顾下 AOP 的核心概念:

  1. Pointcut
    定义一个切点,可以在这个被拦截的方法前后进行切面逻辑。

  2. Advice
    用来定义拦截行为,在这里实现增强的逻辑,它是一个祖先接口 org.aopalliance.aop.Advice。还有其它继承接口,例如 MethodBeforeAdvice ,特定指方法执行前的增强。

  3. Advisor
    用来封装切面的所有信息,主要是上面两个,它用来充当 AdvicePointcut 的适配器。

eaa80b0888fcfc7b55a6ca452db379b5.png
advisor_consist

回顾完 AOP 的概念后,继续来看下这三个关键 bean:

  • TransactionInterceptor: 实现了 Advice 接口,在这里定义了拦截行为。

  • AnnotationTransactionAttributeSource:封装了目标方法是否被拦截的逻辑,虽然没有实现 Pointcut 接口,但是在后面目标方法判断的时候,实际上还是委托给了 AnnotationTransactionAttributeSource.getTransactionAttributeSource,通过适配器模式,返回了 Pointcut 切点信息。

  • TransactionAttributeSourceAdvisor: 实现了 Advisor 接口,包装了上面两个信息。

这三个 bean 组成的结构与 AOP 切面环绕实现的结构一致,所以先学习 AOP 的实现,对事务的了解会有所帮助


接着看我们的自动创建代理器是如何创建的:

AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element)

public static void registerAutoProxyCreatorIfNecessary(
        ParserContext parserContext, Element sourceElement) {

在这一步中,注册了一个 beanNameorg.springframework.aop.config.internalAutoProxyCreatorbeanInfrastructureAdsivorAutoProxyCreator,下图是它的继承体系图:

6a98ea5b8cf74f95230ca25bf234d9f1.png
infrastructrue_advisor_auto_proxy_creator_diagram

可以看到,它实现了 InstantiationAwareBeanPostProcessor 这个接口,也就是说在 Spring 容器中,所有 bean 实例化时,Spring 都会保证调用其 postProcessAfterInitialization 方法。

与上一篇介绍的 AOP 代理器一样,在实例化 bean 的时候,调用了代理器父类 AbstractAutoProxyCreatorpostProcessAfterInitialization 方法:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {

其中关于 wrapIfNecessory 方法,在上一篇 AOP 中已经详细讲过,这里讲下这个方法做了什么工作:

  1. 找出指定 `bean` 对应的增强器

  2. 根据找出的增强器创建代理

与创建 AOP 代理相似的过程就不再重复说,讲下它们的不同点:


判断目标方法是否适合 canApply

AopUtils#canApply(Advisor, Class, boolean)

public static boolean canApply(Advisor advisor, Class> targetClass, boolean hasIntroductions) {

我们在前面看到,TransactionAttributeSourceAdvisor 的父类是 PointcutAdvisor,所以在目标方法判断的时候,会取出切点信息 pca.getPointcut()

我们之前注入的切面类型 beanAnnotationTransactionAttributeSource,通过下面的方法包装,最后返回对象类型是 TransactionAttributeSourcePointcut 的切点信息

private 

匹配标签 match

在匹配 match 操作中,区别的是 AOP 识别的是 @Before@After,而我们的事务 TX 识别的是 @Transactional 标签。

判断是否是事务方法的入口方法在这:

org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut#matches

@Override

那它到底到哪一步解析事务注解的呢,继续跟踪代码,答案是:

AnnotationTransactionAttributeSource#determineTransactionAttribute

protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {

在这一步中,遍历注册的注解解析器进行解析,由于我们关注的是事务解析,所以直接定位到事务注解的解析器:

SpringTransactionAnnotationParser#parseTransactionAnnotation(AnnotatedElement)

public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {

首先判断是否含有 @Transactional 注解,如果有的话,才继续调用 parse 解析方法:

protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {

小结

通过上面的步骤,完成了对应类或者方法的事务属性解析。

主要步骤在于寻找增强器,以及判断这些增强器是否与方法或者类匹配。

如果某个 bean 属于可被事务增强时,也就是适用于增强器 BeanFactoryTransactionAttributeSourceAdvisor 进行增强。

之前我们注入了 TransactionInterceptorBeanFactoryTransactionAttributeSourceAdvisor 中,所以在调用事务增强器增强的代理类时,会执行 TransactionInterceptor 进行增强。同时,也就是在 TransactionInterceptor 类中的 invoke 方法中完成整个事务的逻辑。


运行


事务增强器 TransactionInterceptor

TransactionInterceptor 支撑着整个事务功能的架构。跟之前 AOPJDK 动态代理 分析的一样,TransactionInterceptor 拦截器继承于 MethodInterceptor,所以我们要从它的关键方法 invoke() 看起:

public Object invoke(MethodInvocation invocation) throws Throwable {

实际调用了父类的方法:TransactionAspectSupport#invokeWithinTransaction

protected Object invokeWithinTransaction(Method method, @Nullable Class> targetClass,final InvocationCallback invocation) throws Throwable {

贴出的代码有删减,简略了错误异常的 try / catch 和编程式事务处理的逻辑。因为我们更多使用到的是声明式事务处理,就是在 XML 文件配置或者 @Transactional 注解编码,实际通过 AOP 实现,而编程式事务处理是通过 Transaction Template 实现,比较少使用到,所以省略了这部分处理代码。


事务管理器

通过该方法,确定要用于给定事务的特定事务管理器

TransactionAspectSupport#determineTransactionManager

protected PlatformTransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {

由于最开始我们在 XML 文件中配置过 transactionManager 属性,所以该方法在我们例子中将会返回类型是 DataSourceTransactionManager 的事务管理器,下面是 DataSourceTransactionManager 的继承体系:

4ab4f7b4a460241089d073722299dff9.png
datasource_transaction_manager

它实现了 InitializingBean 接口,不过只是在 afterPropertiesSet() 方法中,简单校验 dataSource 是否为空,不细说这个类。


事务开启

TransactionAspectSupport#createTransactionIfNecessary

protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {

在创建事务方法中,主要完成以下三件事:

  1. 使用 `DelegatingTransactionAttribute` 包装 `txAttr` 实例

  2. 获取事务:`tm.getTransaction(txAttr)`

  3. 构建事务信息:`prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`

核心方法在第二点和第三点,分别摘取核心进行熟悉。


获取 TransactionStatus

status = tm.getTransaction(txAttr);

由于代码较长,直接来总结其中几个关键点

获取事务

创建对应的事务实例,我们使用的是 DataSourceTransactionManager 中的 doGetTransaction 方法,创建基于 JDBC 的事务实例。

protected Object doGetTransaction() {

其中在同一个线程中,判断是否有重复的事务,是在 TransactionSynchronizationManager.getResource(obtainDataSource()) 中完成的,关键判断逻辑是下面这个:

private 

结论:resources 是一个 ThreadLocal 线程私有对象,每个线程独立存储,所以判断是否存在事务,判断的依据是当前线程、当前数据源(DataSource)中是否存在活跃的事务 - map.get(actualKey)


处理已经存在的事务

根据前面说的,判断当前线程是否存在事务,判断依据为当前线程记录的连接不为空且连接中(connectionHolder)中的 transactionActive 属性不为空,如果当前线程存在事务,将根据不同的事务传播特性进行处理。具体代码逻辑如下:

if (isExistingTransaction(transaction)) {

PROPAGATION_NEVER

在配置中配置设定为 PROPAGATION_NEVER,表示该方法需要在非事务的环境下运行,但处于事务处理的状态(可能是外部带事务的方法调用了非事务的方法),将会抛出异常:

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {

PROPAGATION_NOT_SUPPORTED

如果有事务存在,将事务挂起,而不是抛出异常:

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {

事务挂起

对于挂起操作,主要目的是记录原有事务的状态,以便于后续操作对事务的恢复:

实际上,suspend() 方法调用的是事务管理器 DataSourceTransactionManager 中的 doSuspend() 方法

protected Object doSuspend(Object transaction) {

最后调用的关键方法是 TransactionSynchronizationManager#doUnbindResource

private static Object doUnbindResource(Object actualKey) {

看了第七条参考资料中的文章,结合代码理解了事务挂起的操作:移除当前线程、数据源活动事务对象的一个过程

那它是如何实现事务挂起的呢,答案是在 doSuspend() 方法中的 txObject.setConnectionHolder(null),将 connectionHolder 设置为 null

一个 connectionHolder 表示一个数据库连接对象,如果它为 null,表示在下次需要使用时,得从缓存池中获取一个连接,新连接的自动提交是 true


PROPAGATION_REQUIRES_NEW

表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则这个方法运行期间被挂起。

SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    DefaultTransactionStatus status = newTransactionStatus(
            definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    // 新事务的建立
    doBegin(transaction, definition);
    prepareSynchronization(status, definition);
    return status;
}
catch (RuntimeException | Error beginEx) {
    resumeAfterBeginException(transaction, suspendedResources, beginEx);
    throw beginEx;
}

与前一个方法相同的是,在 PROPAGATION_REQUIRES_NEW 广播特性下,也会使用 suspend 方法将原事务挂起。方法 doBegin(),是事务开启的核心。


PROPAGATION_NESTED

表示如果当前正有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像 PROPAGATION_REQUIRES_NEW

在代理处理上,有两个分支,与 PROPAGATION_REQUIRES_NEW 相似的不贴出来,讲下使用 savepoint 保存点的方式事务处理:

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {

学习过数据库的朋友应该清楚 savepoint,可以利用保存点回滚部分事务,从而使事务处理更加灵活和精细。跟踪代码,发现创建保存点调用的方法是 org.hsqldb.jdbc.JDBCConnection#setSavepoint(java.lang.String),感兴趣的可以往下继续深入学习~


事务创建

其实在前面方法中,都出现过这个方法 doBegin(),在这个方法中创建事务,顺便设置数据库的隔离级别、timeout 属性和设置 connectionHolder

DataSourceTransactionManager#doBegin

protected void doBegin(Object transaction, TransactionDefinition definition) {

结论:Spring 事务的开启,就是将数据库自动提交属性设置为 false


小结

在声明式的事务处理中,主要有以下几个处理步骤:

  1. 获取事务的属性tas.getTransactionAttribute(method, targetClass)

  2. 加载配置中配置的 `TransactionManager`determineTransactionManager(txAttr);

  3. 不同的事务处理方式使用不同的逻辑:关于声明式事务和编程式事务,可以查看这篇文章-Spring编程式和声明式事务实例讲解

  4. 在目标方法执行前获取事务并收集事务信息:createTransactionIfNecessary(tm, txAttr, joinpointIdentification)

  5. 执行目标方法invocation.proceed()

  6. 出现异常,尝试异常处理completeTransactionAfterThrowing(txInfo, ex);

  7. 提交事务前的事务信息消除cleanupTransactionInfo(txInfo)

  8. 提交事务commitTransactionAfterReturning(txInfo)


事务回滚 & 提交

这两步操作,主要调用了底层数据库连接的 API,所以没有细说。


总结

本篇文章简单记录了如何使用 Spring 的事务,以及在代码中如何实现。

在之前的使用场景中,只用到了默认配置的声明式事务 @Transactional,不了解其它属性设置的含义,也不知道在默认配置下,如果是同一个类中的方法自调用是不支持事务。

所以,经过这一次学习和总结,在下一次使用时,就能够知道不同属性设置能解决什么问题,例如修改广播特性 PROPAGATION,让事务支持方法自调用,还有设置事务超时时间 timeout、隔离级别等属性。


由于个人技术有限,如果有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正

Gitee 地址 :

https://gitee.com/vip-augus/spring-analysis-note.git

Github 地址 :

https://github.com/Vip-Augus/spring-analysis-note

End

如有收获,请帮忙转发,您的鼓励是作者最大的动力!

7a8e08df72cfd2acf02736c46fa005c3.png c004a39883aa486dbb960e65ccd5ed42.png

我是本周的小编「沉默王二」,现在要隆重地告诉大家一个好消息,对于加入「Java极客技术」知识星球的同学提供基本的福利:

文章有疑问的地方可以提问,其他工作问题都可以提问出来,作者免费作答。

 https://t.zsxq.com/Y3fYny7

每周都有大牛分享一些面试题,和面试注意的知识点!

 https://t.zsxq.com/2bufE2v

每周由Java极客技术独家编制的设计模式与大家分享!

 https://t.zsxq.com/3bUNbEI

每两周还会分享一个话题,和大家一起成长!

 https://t.zsxq.com/BI6Unm2

还有Java极客技术团队亲自录制了一套 Spring Boot 视频,这套视频加密,加密后放到云盘上,下载链接加密之后,一机一码,每个星球的用户一个播放授权码。

我们做知识星球的目的和其他星主一样,就是为了帮助大家一起更好的成长,与高手拉近距离,减少差距,其实你也是高手!

ec1b8a12a532e8cd62b50c609cb2c767.png 05f6a3937646d84623a3621d4b679544.png b7c0c0434b69c6967553cf5780b42cd9.png a786fc84ef9d028362e5f05c3ce3d8dc.png 9db25c4af3560989b308c45500e96621.png

前1000人,50元/每年,现在大约还剩407名额。

长按二维码

61ae7618f1e654a6358bcaa570def5d7.png

7a8e08df72cfd2acf02736c46fa005c3.png

欢迎长按下图关注公众号Java极客技术,后台回复“资料”,获取作者独家秘制精品资料

46eea206e4262971d5983e00bb59b3b4.png

Java 极客技术公众号,是由一群热爱 Java 开发的技术人组建成立,专注分享原创、高质量的 Java 文章。如果您觉得我们的文章还不错,请帮忙赞赏、在看、转发支持,鼓励我们分享出更好的文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值