0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

分享一下Spring事务的玩法

OSC开源社区 来源:江南一点雨 2022-12-30 11:41 次阅读

事务的重要性不言而喻,Spring 对事务也提供了非常丰富的支持,各种支持的属性应有尽有。

然而很多小伙伴知道,这里有两个属性特别绕:

隔离性

传播性

有多绕呢?松哥都一直懒得写文章去总结。不过最近有小伙伴问到这个问题,刚好有空,就抽空总结一下,我不会干巴巴的和大家讲概念,接下来的所有内容,松哥都会通过具体的案例来和大家演示。

好啦,不废话啦,请看大屏幕。

1. 什么是事务

数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。

在我们日常工作中,涉及到事务的场景非常多,一个 service 中往往需要调用不同的 dao 层方法,这些方法要么同时成功要么同时失败,我们需要在 service 层确保这一点。

说到事务最典型的案例就是转账了:

张三要给李四转账 500 块钱,这里涉及到两个操作,从张三的账户上减去 500 块钱,给李四的账户添加 500 块钱,这两个操作要么同时成功要么同时失败,如何确保他们同时成功或者同时失败呢?答案就是事务。

事务有四大特性(ACID):

c15977e4-7619-11ed-8abf-dac502259ad0.png

原子性(Atomicity): 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。

一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。

隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

这就是事务的四大特性。

2. Spring 中的事务

2.1 两种用法

Spring 作为 Java 开发中的基础设施,对于事务也提供了很好的支持,总体上来说,Spring 支持两种类型的事务,声明式事务和编程式事务。

编程式事务类似于 Jdbc 事务的写法,需要将事务的代码嵌入到业务逻辑中,这样代码的耦合度较高,而声明式事务通过 AOP 的思想能够有效的将事务和业务逻辑代码解耦,因此在实际开发中,声明式事务得到了广泛的应用,而编程式事务则较少使用,考虑到文章内容的完整,本文对两种事务方式都会介绍。

2.2 三大基础设施

Spring 中对事务的支持提供了三大基础设施,我们先来了解下。

PlatformTransactionManager

TransactionDefinition

TransactionStatus

这三个核心类是 Spring 处理事务的核心类。

2.2.1 PlatformTransactionManager

PlatformTransactionManager 是事务处理的核心,它有诸多的实现类,如下:

c180036e-7619-11ed-8abf-dac502259ad0.png

PlatformTransactionManager 的定义如下:

publicinterfacePlatformTransactionManager{
TransactionStatusgetTransaction(@NullableTransactionDefinitiondefinition);
voidcommit(TransactionStatusstatus)throwsTransactionException;
voidrollback(TransactionStatusstatus)throwsTransactionException;
}

可以看到 PlatformTransactionManager 中定义了基本的事务操作方法,这些事务操作方法都是平台无关的,具体的实现都是由不同的子类来实现的。

这就像 JDBC 一样,SUN 公司制定标准,其他数据库厂商提供具体的实现。

这么做的好处就是我们 Java 程序员只需要掌握好这套标准即可,不用去管接口的具体实现。以 PlatformTransactionManager 为例,它有众多实现,如果你使用的是 JDBC 那么可以将 DataSourceTransactionManager 作为事务管理器;如果你使用的是 Hibernate,那么可以将 HibernateTransactionManager 作为事务管理器;如果你使用的是 JPA,那么可以将 JpaTransactionManager 作为事务管理器。

DataSourceTransactionManager、HibernateTransactionManager 以及 JpaTransactionManager 都是 PlatformTransactionManager 的具体实现,但是我们并不需要掌握这些具体实现类的用法,我们只需要掌握好 PlatformTransactionManager 的用法即可。

PlatformTransactionManager 中主要有如下三个方法:

1.getTransaction()

getTransaction() 是根据传入的 TransactionDefinition 获取一个事务对象,TransactionDefinition 中定义了一些事务的基本规则,例如传播性、隔离级别等。

2.commit()

commit() 方法用来提交事务。

3.rollback()

rollback() 方法用来回滚事务。

2.2.2 TransactionDefinition

TransactionDefinition 用来描述事务的具体规则,也称作事务的属性。事务有哪些属性呢?看下图:

c1a5ce1e-7619-11ed-8abf-dac502259ad0.png

可以看到,主要是五种属性:

隔离性

传播性

回滚规则

超时时间

是否只读

这五种属性接下来松哥会和大家详细介绍。

TransactionDefinition 类中的方法如下:

c1ccffe8-7619-11ed-8abf-dac502259ad0.png

可以看到一共有五个方法:

getIsolationLevel(),获取事务的隔离级别

getName(),获取事务的名称

getPropagationBehavior(),获取事务的传播性

getTimeout(),获取事务的超时时间

isReadOnly(),获取事务是否是只读事务

TransactionDefinition 也有诸多的实现类,如下:

c1ecccce-7619-11ed-8abf-dac502259ad0.png

如果开发者使用了编程式事务的话,直接使用 DefaultTransactionDefinition 即可。

2.2.3 TransactionStatus

TransactionStatus 可以直接理解为事务本身,该接口源码如下:

publicinterfaceTransactionStatusextendsSavepointManager,Flushable{
booleanisNewTransaction();
booleanhasSavepoint();
voidsetRollbackOnly();
booleanisRollbackOnly();
voidflush();
booleanisCompleted();
}

isNewTransaction() 方法获取当前事务是否是一个新事务。

hasSavepoint() 方法判断是否存在 savePoint()。

setRollbackOnly() 方法设置事务必须回滚。

isRollbackOnly() 方法获取事务只能回滚。

flush() 方法将底层会话中的修改刷新到数据库,一般用于 Hibernate/JPA 的会话,对如 JDBC 类型的事务无任何影响。

isCompleted() 方法用来获取是一个事务是否结束。

这就是 Spring 中支持事务的三大基础设施。

3. 编程式事务

我们先来看看编程式事务怎么玩。

通过 PlatformTransactionManager 或者 TransactionTemplate 可以实现编程式事务。如果是在 Spring Boot 项目中,这两个对象 Spring Boot 会自动提供,我们直接使用即可。

但是如果是在传统的 SSM 项目中,则需要我们通过配置来提供这两个对象,松哥给一个简单的配置参考,如下(简单起见,数据库操作我们使用 JdbcTemplate):

有了这两个对象,接下来的代码就简单了:

@Service
publicclassTransferService{
@Autowired
JdbcTemplatejdbcTemplate;
@Autowired
PlatformTransactionManagertxManager;

publicvoidtransfer(){
DefaultTransactionDefinitiondefinition=newDefaultTransactionDefinition();
TransactionStatusstatus=txManager.getTransaction(definition);
try{
jdbcTemplate.update("updateusersetaccount=account+100whereusername='zhangsan'");
inti=1/0;
jdbcTemplate.update("updateusersetaccount=account-100whereusername='lisi'");
txManager.commit(status);
}catch(DataAccessExceptione){
e.printStackTrace();
txManager.rollback(status);
}
}
}

这段代码很简单,没啥好解释的,在 try...catch... 中进行业务操作,没问题就 commit,有问题就 rollback。

如果我们需要配置事务的隔离性、传播性等,可以在 DefaultTransactionDefinition 对象中进行配置。

上面的代码是通过 PlatformTransactionManager 实现的编程式事务,我们也可以通过 TransactionTemplate 来实现编程式事务,如下:

@Service
publicclassTransferService{
@Autowired
JdbcTemplatejdbcTemplate;
@Autowired
TransactionTemplatetranTemplate;
publicvoidtransfer(){
tranTemplate.execute(newTransactionCallbackWithoutResult(){
@Override
protectedvoiddoInTransactionWithoutResult(TransactionStatusstatus){
try{
jdbcTemplate.update("updateusersetaccount=account+100whereusername='zhangsan'");
inti=1/0;
jdbcTemplate.update("updateusersetaccount=account-100whereusername='lisi'");
}catch(DataAccessExceptione){
status.setRollbackOnly();
e.printStackTrace();
}
}
});
}
}

直接注入 TransactionTemplate,然后在 execute 方法中添加回调写核心的业务即可,当抛出异常时,将当前事务标注为只能回滚即可。

注意,execute 方法中,如果不需要获取事务执行的结果,则直接使用 TransactionCallbackWithoutResult 类即可,如果要获取事务执行结果,则使用 TransactionCallback 即可。

这就是两种编程式事务的玩法。

编程式事务由于代码入侵太严重了,因为在实际开发中使用的很少,我们在项目中更多的是使用声明式事务。

4. 声明式事务

声明式事务如果使用 XML 配置,可以做到无侵入;如果使用 Java 配置,也只有一个 @Transactional 注解侵入而已,相对来说非常容易。

以下配置针对传统 SSM 项目(因为在 Spring Boot 项目中,事务相关的组件已经配置好了):

4.1 XML 配置

XML 配置声明式事务大致上可以分为三个步骤,如下:

配置事务管理器

配置事务通知

配置 AOP

第二步和第三步中定义出来的方法交集,就是我们要添加事务的方法。

配置完成后,如下一些方法就自动具备事务了:

publicclassUserService{
publicvoidm3(){
jdbcTemplate.update("updateusersetmoney=997whereusername=?","zhangsan");
}
}

4.2 Java 配置

我们也可以使用 Java 配置来实现声明式事务:

@Configuration
@ComponentScan
//开启事务注解支持
@EnableTransactionManagement
publicclassJavaConfig{
@Bean
DataSourcedataSource(){
DriverManagerDataSourceds=newDriverManagerDataSource();
ds.setPassword("123");
ds.setUsername("root");
ds.setUrl("jdbc///test01?serverTimezone=Asia/Shanghai");
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
returnds;
}

@Bean
JdbcTemplatejdbcTemplate(DataSourcedataSource){
returnnewJdbcTemplate(dataSource);
}

@Bean
PlatformTransactionManagertransactionManager(){
returnnewDataSourceTransactionManager(dataSource());
}
}

这里要配置的东西其实和 XML 中配置的都差不多,最最关键的就两个:

事务管理器 PlatformTransactionManager。

@EnableTransactionManagement 注解开启事务支持。

配置完成后,接下来,哪个方法需要事务就在哪个方法上添加 @Transactional 注解即可,向下面这样:

@Transactional(noRollbackFor=ArithmeticException.class)
publicvoidupdate4(){
jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",998,"lisi");
inti=1/0;
}

当然这个稍微有点代码入侵,不过问题不大,日常开发中这种方式使用较多。

当@Transactional 注解加在类上面的时候,表示该类的所有方法都有事务,该注解加在方法上面的时候,表示该方法有事务。

4.3 混合配置

也可以 Java 代码和 XML 混合配置来实现声明式事务,就是一部分配置用 XML 来实现,一部分配置用 Java 代码来实现:

假设 XML 配置如下:

那么 Java 代码中的配置如下:

@Configuration
@ComponentScan
@ImportResource(locations="classpath:applicationContext3.xml")
publicclassJavaConfig{
@Bean
DataSourcedataSource(){
DriverManagerDataSourceds=newDriverManagerDataSource();
ds.setPassword("123");
ds.setUsername("root");
ds.setUrl("jdbc///test01?serverTimezone=Asia/Shanghai");
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
returnds;
}

@Bean
JdbcTemplatejdbcTemplate(DataSourcedataSource){
returnnewJdbcTemplate(dataSource);
}

@Bean
PlatformTransactionManagertransactionManager(){
returnnewDataSourceTransactionManager(dataSource());
}
}

Java 配置中通过 @ImportResource 注解导入了 XML 配置,XML 配置中的内容就是开启 @Transactional 注解的支持,所以 Java 配置中省略了 @EnableTransactionManagement 注解。

这就是声明式事务的几种配置方式。好玩吧!

5. 事务属性

在前面的配置中,我们只是简单说了事务的用法,并没有和大家详细聊一聊事务的一些属性细节,那么接下来我们就来仔细捋一捋事务中的五大属性。

5.1 隔离性

首先就是事务的隔离性,也就是事务的隔离级别。

MySQL 中有四种不同的隔离级别,这四种不同的隔离级别在 Spring 中都得到了很好的支持。

Spring 中默认的事务隔离级别是 default,即数据库本身的隔离级别是啥就是啥,default 就能满足我们日常开发中的大部分场景。

不过如果项目有需要,我们也可以调整事务的隔离级别。

调整方式如下:

5.1.1 编程式事务隔离级别

如果是编程式事务,通过如下方式修改事务的隔离级别:

TransactionTemplate

transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);

TransactionDefinition 中定义了各种隔离级别。

PlatformTransactionManager

publicvoidupdate2(){
//创建事务的默认配置
DefaultTransactionDefinitiondefinition=newDefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionStatusstatus=platformTransactionManager.getTransaction(definition);
try{
jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",999,"zhangsan");
inti=1/0;
//提交事务
platformTransactionManager.commit(status);
}catch(DataAccessExceptione){
e.printStackTrace();
//回滚
platformTransactionManager.rollback(status);
}
}

这里是在 DefaultTransactionDefinition 对象中设置事务的隔离级别。

5.1.2 声明式事务隔离级别

如果是声明式事务通过如下方式修改隔离级别:

XML:

Java:

@Transactional(isolation=Isolation.SERIALIZABLE)
publicvoidupdate4(){
jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",998,"lisi");
inti=1/0;
}

关于事务的隔离级别,如果大家还不熟悉,可以参考松哥之前的文章:四个案例看懂 MySQL 事务隔离级别。

5.2 传播性

先来说说何谓事务的传播性:

事务传播行为是为了解决业务层方法之间互相调用的事务问题,当一个事务方法被另一个事务方法调用时,事务该以何种状态存在?例如新方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行,等等,这些规则就涉及到事务的传播性。

关于事务的传播性,Spring 主要定义了如下几种:

publicenumPropagation{
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
NEVER(TransactionDefinition.PROPAGATION_NEVER),
NESTED(TransactionDefinition.PROPAGATION_NESTED);
privatefinalintvalue;
Propagation(intvalue){this.value=value;}
publicintvalue(){returnthis.value;}
}

具体含义如下:

传播性 描述
REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起
NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起
NEVER 以非事务方式运行,如果当前存在事务,则抛出异常
NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED

一共是七种传播性,具体配置也简单:

TransactionTemplate中的配置

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

PlatformTransactionManager中的配置

publicvoidupdate2(){
//创建事务的默认配置
DefaultTransactionDefinitiondefinition=newDefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatusstatus=platformTransactionManager.getTransaction(definition);
try{
jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",999,"zhangsan");
inti=1/0;
//提交事务
platformTransactionManager.commit(status);
}catch(DataAccessExceptione){
e.printStackTrace();
//回滚
platformTransactionManager.rollback(status);
}
}

声明式事务的配置(XML)

声明式事务的配置(Java)

@Transactional(noRollbackFor=ArithmeticException.class,propagation=Propagation.REQUIRED)
publicvoidupdate4(){
jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",998,"lisi");
inti=1/0;
}

用就是这么来用,至于七种传播的具体含义,松哥来和大家一个一个说。

5.2.1 REQUIRED

REQUIRED 表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

例如我有如下一段代码:

@Service
publicclassAccountService{
@Autowired
JdbcTemplatejdbcTemplate;
@Transactional
publicvoidhandle1(){
jdbcTemplate.update("updateusersetmoney=?whereid=?;",1,2);
}
}
@Service
publicclassAccountService2{
@Autowired
JdbcTemplatejdbcTemplate;
@Autowired
AccountServiceaccountService;
publicvoidhandle2(){
jdbcTemplate.update("updateusersetmoney=?whereusername=?;",1,"zhangsan");
accountService.handle1();
}
}

我在 handle2 方法中调用 handle1。

那么:

如果 handle2 方法本身是有事务的,则 handle1 方法就会加入到 handle2 方法所在的事务中,这样两个方法将处于同一个事务中,一起成功或者一起失败(不管是 handle2 还是 handle1 谁抛异常,都会导致整体回滚)。

如果 handle2 方法本身是没有事务的,则 handle1 方法就会自己开启一个新的事务,自己玩。

举一个简单的例子:handle2 方法有事务,handle1 方法也有事务(小伙伴们根据前面的讲解自行配置事务),项目打印出来的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.spring_tran02.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]forJDBCtransaction
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;]
o.s.jdbc.support.JdbcTransactionManager:Participatinginexistingtransaction
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;]
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]aftertransaction

从日志中可以看到,前前后后一共就开启了一个事务,日志中有这么一句:

Participatinginexistingtransaction

这个就说明 handle1 方法没有自己开启事务,而是加入到 handle2 方法的事务中了。

5.2.2 REQUIRES_NEW

REQUIRES_NEW 表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。换言之,不管外部方法是否有事务,REQUIRES_NEW 都会开启自己的事务。

这块松哥要多说两句,有的小伙伴可能觉得 REQUIRES_NEW 和 REQUIRED 太像了,似乎没啥区别。

其实你要是单纯看最终回滚效果,可能确实看不到啥区别。

但是,大家注意松哥上面的加粗,在 REQUIRES_NEW 中可能会同时存在两个事务,外部方法的事务被挂起,内部方法的事务独自运行,而在 REQUIRED 中则不会出现这种情况,如果内外部方法传播性都是 REQUIRED,那么最终也只是一个事务。

还是上面那个例子,假设 handle1 和 handle2 方法都有事务,handle2 方法的事务传播性是 REQUIRED,而 handle1 方法的事务传播性是 REQUIRES_NEW,那么最终打印出来的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.spring_tran02.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]forJDBCtransaction
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;]
o.s.jdbc.support.JdbcTransactionManager:Suspendingcurrenttransaction,creatingnewtransactionwithname[org.javaboy.spring_tran02.AccountService.handle1]
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]forJDBCtransaction
com.zaxxer.hikari.pool.HikariPool:HikariPool-1-Addedconnectioncom.mysql.cj.jdbc.ConnectionImpl@14ad4b95
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;]
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]aftertransaction
o.s.jdbc.support.JdbcTransactionManager:Resumingsuspendedtransactionaftercompletionofinnertransaction
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]aftertransaction

分析这段日志我们可以看到:

首先为 handle2 方法开启了一个事务。

执行完 handle2 方法的 SQL 之后,事务被刮起(Suspending)。

为 handle1 方法开启了一个新的事务。

执行 handle1 方法的 SQL。

提交 handle1 方法的事务。

恢复被挂起的事务(Resuming)。

提交 handle2 方法的事务。

从这段日志中大家可以非常明确的看到 REQUIRES_NEW 和 REQUIRED 的区别。

松哥再来简单总结下(假设 handle1 方法的事务传播性是 REQUIRES_NEW):

如果 handle2 方法没有事务,handle1 方法自己开启一个事务自己玩。

如果 handle2 方法有事务,handle1 方法还是会开启一个事务。此时,如果 handle2 发生了异常进行回滚,并不会导致 handle1 方法回滚,因为 handle1 方法是独立的事务;如果 handle1 方法发生了异常导致回滚,并且 handle1 方法的异常没有被捕获处理传到了 handle2 方法中,那么也会导致 handle2 方法回滚。

这个地方小伙伴们要稍微注意一下,我们测试的时候,由于是两个更新 SQL,如果更新的查询字段不是索引字段,那么 InnoDB 将使用表锁,这样就会发生死锁(handle2 方法执行时开启表锁,导致 handle1 方法陷入等待中,而必须 handle1 方法执行完,handle2 才能释放锁)。所以,在上面的测试中,我们要将 username 字段设置为索引字段,这样默认就使用行锁了。

5.2.3 NESTED

NESTED 表示如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。

假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NESTED,那么最终执行的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]forJDBCtransaction
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;]
o.s.jdbc.support.JdbcTransactionManager:Creatingnestedtransactionwithname[org.javaboy.demo.AccountService.handle1]
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;]
o.s.jdbc.support.JdbcTransactionManager:Releasingtransactionsavepoint
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]aftertransaction

关键一句在 Creating nested transaction。

此时,NESTED 修饰的内部方法(handle1)属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务(需要处理掉内部子事务的异常)。

5.2.4 MANDATORY

MANDATORY 表示如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

这个好理解,我举两个例子:

假设 handle2 方法有事务,handle1 方法也有事务且传播性为 MANDATORY,那么最终执行的事务日志如下:

o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]forJDBCtransaction
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;]
o.s.jdbc.support.JdbcTransactionManager:Participatinginexistingtransaction
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;]
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]aftertransaction

从这段日志可以看出:

首先给 handle2 方法开启事务。

执行 handle2 方法的 SQL。

handle1 方法加入到已经存在的事务中。

执行 handle1 方法的 SQL。

提交事务。

假设 handle2 方法无事务,handle1 方法有事务且传播性为 MANDATORY,那么最终执行时会抛出如下异常:

Noexistingtransactionfoundfortransactionmarkedwithpropagation'mandatory'

由于没有已经存在的事务,所以出错了。

5.2.5 SUPPORTS

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

这个也简单,举两个例子大家就明白了。

假设 handle2 方法有事务,handle1 方法也有事务且传播性为 SUPPORTS,那么最终事务执行日志如下:

o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]forJDBCtransaction
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;]
o.s.jdbc.support.JdbcTransactionManager:Participatinginexistingtransaction
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;]
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]aftertransaction

这段日志很简单,没啥好说的,认准 Participating in existing transaction 表示加入到已经存在的事务中即可。

假设 handle2 方法无事务,handle1 方法有事务且传播性为 SUPPORTS,这个最终就不会开启事务了,也没有相关日志。

5.2.6 NOT_SUPPORTED

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

假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NOT_SUPPORTED,那么最终事务执行日志如下:

o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]forJDBCtransaction
o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]tomanualcommit
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;]
o.s.jdbc.support.JdbcTransactionManager:Suspendingcurrenttransaction
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate
o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;]
o.s.jdbc.datasource.DataSourceUtils:FetchingJDBCConnectionfromDataSource
o.s.jdbc.support.JdbcTransactionManager:Resumingsuspendedtransactionaftercompletionofinnertransaction
o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit
o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]
o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]aftertransaction

这段日志大家认准这两句就行了 :Suspending current transaction 表示挂起当前事务;Resuming suspended transaction 表示恢复挂起的事务。

5.2.7 NEVER

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

假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NEVER,那么最终会抛出如下异常:

Existingtransactionfoundfortransactionmarkedwithpropagation'never'

5.3 回滚规则

默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)以及 Error 时才会回滚,在遇到检查型(Checked Exception)异常时不会回滚。

像 1/0,空指针这些是 RuntimeException,而 IOException 则算是 Checked Exception,换言之,默认情况下,如果发生 IOException 并不会导致事务回滚。

如果我们希望发生 IOException 时也能触发事务回滚,那么可以按照如下方式配置:

Java 配置:

@Transactional(rollbackFor=IOException.class)
publicvoidhandle2(){
jdbcTemplate.update("updateusersetmoney=?whereusername=?;",1,"zhangsan");
accountService.handle1();
}

XML 配置:

另外,我们也可以指定在发生某些异常时不回滚,例如当系统抛出 ArithmeticException 异常并不要触发事务回滚,配置方式如下:

Java 配置:

@Transactional(noRollbackFor=ArithmeticException.class)
publicvoidhandle2(){
jdbcTemplate.update("updateusersetmoney=?whereusername=?;",1,"zhangsan");
accountService.handle1();
}

XML 配置:

5.4 是否只读

只读事务一般设置在查询方法上,但不是所有的查询方法都需要只读事务,要看具体情况。

一般来说,如果这个业务方法只有一个查询 SQL,那么就没必要添加事务,强行添加最终效果适得其反。

但是如果一个业务方法中有多个查询 SQL,情况就不一样了:多个查询 SQL,默认情况下,每个查询 SQL 都会开启一个独立的事务,这样,如果有并发操作修改了数据,那么多个查询 SQL 就会查到不一样的数据。此时,如果我们开启事务,并设置为只读事务,那么多个查询 SQL 将被置于同一个事务中,多条相同的 SQL 在该事务中执行将会获取到相同的查询结果。

设置事务只读的方式如下:

Java 配置:

@Transactional(readOnly=true)

XML 配置:

5.5 超时时间

超时时间是说一个事务允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。

事务超时时间配置方式如下(单位为秒):

Java 配置:

@Transactional(timeout=10)

XML 配置:

在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。

6. 注意事项

事务只能应用到 public 方法上才会有效。

事务需要从外部调用,Spring 自调事务用会失效。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效,这点尤其要注意,因为代理模式只拦截通过代理传入的外部方法调用,所以自调用事务是不生效的。

建议事务注解 @Transactional 一般添加在实现类上,而不要定义在接口上,如果加在接口类或接口方法上时,只有配置基于接口的代理这个注解才会生效。

7. 小结

好啦,这就是和大家分享的 Spring 事务的玩法,不知道小伙伴们搞明白没有?






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2957

    浏览量

    104539
  • XML
    XML
    +关注

    关注

    0

    文章

    188

    浏览量

    33040
  • Boot
    +关注

    关注

    0

    文章

    149

    浏览量

    35778

原文标题:长文捋明白Spring事务!隔离性?传播性?一网打尽!

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Spring事务失效的十种常见场景

    Spring针对Java Transaction API (JTA)、JDBC、Hibernate和Java Persistence API(JPA)等事务 API,实现了致的编程模型,而
    的头像 发表于 12-11 15:03 854次阅读

    Spring事务实现原理

    作者:京东零售 范锡军 1、引言 springspring-tx模块提供了对事务管理支持,使用spring事务可以让我们从复杂的
    的头像 发表于 11-08 10:10 727次阅读
    <b class='flag-5'>Spring</b><b class='flag-5'>事务</b>实现原理

    什么是java spring

    水平,学习和研究Spring源码将会使你收到意想不到的效果。Spring框架的好处在我们进入细节以前,让我们看一下Spring可以给个工程
    发表于 09-11 11:16

    Spring的两种方式事务管理和API接口介绍

    Spring事务管理
    发表于 03-21 06:52

    Spring事务分析的实现方式

    Spring事务原理分析
    发表于 07-02 15:19

    详解Spring事务管理

    在学习spring事务管理时,我忍不住要问,spring为什么进行事务管理,spring怎么进行的事务
    发表于 07-12 06:54

    Spring事务管理详解说明

    Spring事务管理详解
    发表于 05-20 13:46

    spring中声明式事务实现原理猜想

      @Transactional注解简介 @Transactional 是spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚。 @Transactional 注解可以帮助
    的头像 发表于 10-13 09:20 1612次阅读

    浅谈Spring事务的那些坑

    对于从事java开发工作的同学来说,spring事务肯定再熟悉不过了。在某些业务场景,如果同时有多张表的写入操作,为了保证操作的原子性(要么同时成功,要么同时失败)避免数据不致的
    的头像 发表于 10-11 10:31 721次阅读

    发现Spring事务的巨坑bug 你必须要小心了

    不正确 9.多线程调用 10.嵌套事务多回滚了 对于从事java开发工作的同学来说,spring事务肯定再熟悉不过了。在某些业务场景,如果同时有多张表的写入操作,为了保证操作的原子
    的头像 发表于 10-11 18:17 835次阅读

    浅谈Spring事务底层原理

    开启Spring事务本质上就是增加了个Advisor,但我们使用@EnableTransactionManagement注解来开启Spring事务
    的头像 发表于 12-06 09:56 672次阅读

    Spring事务在哪几种情况会不生效?

    日常开发中,我们经常使用到spring事务。最近星球位还有去美团面试,被问了这么道面试题: Spring
    的头像 发表于 05-10 17:53 894次阅读
    <b class='flag-5'>Spring</b><b class='flag-5'>事务</b>在哪几种情况<b class='flag-5'>下</b>会不生效?

    8个Spring事务失效的场景介绍

    事故。今天,我们就简单来说下Spring事务的原理,然后总结一下spring事务失败的场景,并提出对应的解决方案。
    的头像 发表于 05-11 10:41 596次阅读
    8个<b class='flag-5'>Spring</b><b class='flag-5'>事务</b>失效的场景介绍

    spring事务失效的些场景

    对于从事java开发工作的同学来说,spring事务肯定再熟悉不过了。 在某些业务场景,如果个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败)
    的头像 发表于 10-08 14:27 423次阅读
    <b class='flag-5'>spring</b><b class='flag-5'>事务</b>失效的<b class='flag-5'>一</b>些场景

    Spring事务传播性的相关知识

    本文主要介绍了Spring事务传播性的相关知识。
    的头像 发表于 01-10 09:29 398次阅读
    <b class='flag-5'>Spring</b><b class='flag-5'>事务</b>传播性的相关知识