【Spring框架】Spring事务的介绍与使用方法
⚠️ 再提醒一次:Spring 本身并不实现事务,Spring事务 的本质还是底层数据库对事务的支持。你的程序是否支持事务首先取决于数据库 ,比如使用 MySQL 的话,如果你选择的是 innodb 引擎,那么恭喜你,是可以支持事务的。但是,如果你的 MySQL 数据库使用的是 myisam 引擎的话,那不好意思,Spring从根上就是不支持事务的。
这里再多提一下一个非常重要的知识点:MySQL 怎么保证原子性的?
我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚之前未完成的事务。
〇、Spring 事务简介
Spring 事务 提供一套抽象的事务管理,并且结合 Spring IoC 和 Spring AOP,简化了应用程序使用数据库事务,通过声明式事务,可以做到对应用程序无侵入的实现事务功能。例如 使用JDBC 操作数据库,想要使用事务的步骤为:
- 获取连接 Connection con = DriverManager.getConnection()
- 开启事务con.setAutoCommit(true/false);
- 执行CRUD
- 提交事务/回滚事务 con.commit() / con.rollback();
- 关闭连接 conn.close();
采用Spring 事务后,只需要 关注第3步的实现,其他的步骤 都是Spring 完成。
Spring事务的本质 其实就是 AOP 和 数据库事务,Spring 将数据库的事务操作提取为切面,通过AOP的方式增强事务方法。
一、Spring 支持两种方式的事务管理
Spring提供了编程式事务和声明式事务两种实现方式。
1.1 编程式事务管理
Spring编程式事务有两种实现方法:1、TransactionTemplate 2、PlatformTransactionManager
对于编程式事务管理,Spring推荐使用TransactionTemplate。
通过 TransactionTemplate或者PlatformTransactionManager 手动管理事务,实际应用中很少使用,现在大多都是用声明式事务通过Spring自动帮我们管理事务,但是编程式事务对于你理解 Spring 事务管理原理有帮助。
1.1.1 TransactionTemplate
采用TransactionTemplate和采用其他Spring模板,如JdbcTempalte和HibernateTemplate来实现事务是一样的,都属于编程式事务。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate是线程安全的。实例代码如下:
1、无返回值
// 可以自己手动新建,也可以将TransactionTemplate交给Spring容器管理,直接通过@Autowired自动注入得到transactionTemplate对象
TransactionTemplate transactionTemplate = new TransactionTemplate();
// 如果无返回值,就new TransactionCallbackWithoutResult()
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// .... 业务代码
jdbcTemplate.update("update t set v=? where k=?", "n", "k2");
//不需要手动提交
}catch (Exception e){
e.printStackTrace();
//但是需要手动回滚
status.setRollbackOnly();
}
}
});
2、有返回值
// 新建一个TransactionTemplate
TransactionTemplate tt = new TransactionTemplate();
// 如果有返回值,就new TransactionCallback()
Object result = tt.execute(
new TransactionCallback(){
public Object doTransaction(TransactionStatus status){
// 业务逻辑...
updateOperation();
// 返回值
return resultOfUpdateOperation();
}
}
); // 执行execute方法进行事务管理
注: 使用TransactionCallback()可以返回一个值。如果使用TransactionCallbackWithoutResult则没有返回值。
使用TransactionTemplate来实现编程式事务需要自己创建事务,但是不需要自己手动提交,它如果正常执行完会自动提交事务。不过需要自己手动设置回滚,如果不设置回滚事务出现问题是不会回滚的。
1.1.2 PlatformTransactionManager
Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。所以Spring事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、JPA,它们自己去按照接口模板实现自己的事务管理器。示例代码如下:
1、可以自己手动创建TransactionManager对象
// 定义一个某个框架平台的TransactionManager,如JDBC、Hibernate
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
// 设置数据源
dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource());
// 定义事务属性
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
// 设置传播行为属性
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
// 获得事务状态,开始事务
TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef);
try {
// 业务逻辑:数据库操作
doSomething();
// 提交事务
dataSourceTransactionManager.commit(status);
} catch (Exception e) {
// 回滚回滚
dataSourceTransactionManager.rollback(status);
}
2、也可以将TransactionManager对象交给Spring去管理,通过@Autowired自动注入获得事务管理器对象
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
使用TransactionManager来实现编程式事务与TransactionTemplate有些许不同,它依旧需要自己创建事务,需要自己手动设置回滚。并且它还需要自己需手动提交事务,如果不手动提交,事务是不会自动提交的。
1.2 声明式事务管理
声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
推荐使用声明式事务,因为代码侵入性最小,底层实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)。声明式事务并不需要自己创建事务,提交事务,回滚等操作,Spring会自动替你完成,但是声明式事务的粒度就是方法层面的,而编程式事务的粒度可以更小。
既然是基于AOP实现的,那么使用声明式事务必然需要对事务进行配置,根据不同的配置方法,可以总结出三种实现方法:1、XML实现 2、注解实现 3、混合实现(XML和注解同时使用)
其实我们能发现这三种实现方法和AOP的配置是一样的,Spring事务的底层原理就是通过AOP代理增强代码实现的,在原有代码上增加了事务管理的代码。
1.2.1 xml配置声明式事务
XML配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
Index of /schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
Index of /schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
Index of /schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 纯xml 我们的业务类也配成xml -->
<!--将业务类交给spring管理-->
<bean id="xmlUserService" class="com.test.transaction.service.XMLUserService">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--配置jdbcTemplate,来设置数据源,并将jdbcTemplate装配进Spring容器-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="datasource"></property>
</bean>
<!--配置数据源,将数据源绑定到jdbcTemplate-->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="url" value="jdbc:mysql://localhost:3306/shadow?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC">
</property>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver">
</property>
</bean>
<!--配置事务管理器,将事务管理器绑定上数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasource"></property>
</bean>
<!--事务通知(设置在什么时候干什么事),将事务管理器作用到指定的方法中-->
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="update"/>
</tx:attributes>
</tx:advice>
<!-- aop的配置,设置切入点,表示对包路径com.test.transaction.service下的所有名为update的方法进行事务管理-->
<aop:config>
<aop:pointcut id="p" expression="execution(*com.test.transaction.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
</aop:config>
</beans>
Java代码:
public class XMLUserService {
JdbcTemplate jdbcTemplate;
// 通过xml配置注入jdbcTemplate
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// Spring会自动对update方法进行事务管理
public void update() {
jdbcTemplate.update("update t set v=? where k=?","n","k1");
// 检测到抛出异常就会自动回滚事务
throw new NullPointerException();
}
}
1.2.2 注解配置声明式事务
JavaConfig配置类:
// 将App这个类作为JavaConfig配置类.这个注解如果不加会有问题
@Configuration
// 开启注解支撑
@EnableTransactionManagement
// 扫描这个包下面的注解
@ComponentScan("com.test.transaction")
public class App {
// 将jdbcTemplate对象放到容器当中,用于连接数据库,执行sql语句
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
// 将数据源对象配置到Spring容器中
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql://localhost:3306/shadow?
useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC");
dataSource.setUsername("root");
return dataSource;
}
/**
* 声明命事务管理器,将事务管理器装配到Spring容器中
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
// 创建事务管理器对象
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
// 设置数据源
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
}
使用 @Transactional注解进行事务管理的示例代码如下:
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
注解@Transactional的属性
具体位置在:org.springframework.transaction.annotation.Transactional
1.2.3 混合方式
这个就没有标准,看自己的喜好哪些写到xml,哪些写成Java代码。
以下配置代码参考自Spring事务配置的五种方式。根据代理机制的不同,总结了五种Spring事务的配置方式。他们都是可以实现XML和注解混用的。
1、每个Bean都有一个代理
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
Index of /schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
Index of /schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!-- 配置注解扫描 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定义事务管理器(声明式的事务) -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 配置DAO -->
<bean id="userDaoTarget" class="xxx.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 配置事务代理 -->
<bean id="userDao" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 配置事务管理器 -->
<!-- 将事务管理添加到DAO层代码上 -->
<property name="transactionManager" ref="transactionManager" />
<property name="target" ref="userDaoTarget" />
<property name="proxyInterfaces" value="xxx.dao.GeneratorDao" />
<!-- 配置事务属性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
2、所有Bean共享一个代理基类
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
Index of /schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
Index of /schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!-- 配置数据源 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定义事务管理器(声明式的事务) -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 配置事务基类 -->
<bean id="transactionBase" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" lazy-init="true" abstract="true">
<!-- 配置事务管理器 -->
<property name="transactionManager" ref="transactionManager" />
<!-- 配置事务属性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<!-- 配置DAO -->
<bean id="userDaoTarget" class="xxx.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="userDao" parent="transactionBase" >
<property name="target" ref="userDaoTarget" />
</bean>
</beans>
3、使用拦截器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
Index of /schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
Index of /schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!-- 配置数据源 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定义事务管理器(声明式的事务) -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 配置事务拦截器 -->
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager" />
<!-- 配置事务属性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>*Dao</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
<!-- 配置DAO -->
<bean id="userDao" class="xxx.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
</beans>
4、使用tx标签配置的拦截器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
Index of /schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
Index of /schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!-- 开启注解支持 -->
<context:annotation-config />
<!-- 扫描注解 现在可以不写上面那句,直接写下面的扫描注解标签就可以开启注解支持了-->
<context:component-scan base-package="xxx" />
<!-- 定义数据源 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定义事务管理器(声明式的事务) -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 定义事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<!-- 定义切面 -->
<aop:config>
<aop:pointcut id="interceptorPointCuts" expression="execution(* xxx.dao.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="interceptorPointCuts" />
</aop:config>
</beans>
5、全注解
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
Index of /schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
Index of /schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!-- 1.配置数据源 -->
<context:annotation-config />
<!-- 2.配置事务管理器 -->
<context:component-scan base-package="xxx" />
<!-- 3.配置事务管理器 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
</bean>
<!-- 定义事务管理器(声明式的事务) -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- 启用注解方式的声明式事务,如果将这个去掉,将不会创建事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
然后在需要使用事务的类或方法上加上注解@Transactional即可。
1.3 编程式事务和声明式事务的区别
编程式事务每次实现都要单独实现,业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。
二、Spring 事务管理接口介绍
Spring 框架中,事务管理相关最重要的 3 个接口如下:
- PlatformTransactionManager:(平台)事务管理器,Spring 事务策略的核心。
- TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
- TransactionStatus:事务运行状态。
我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinition 和 TransactionStatus 这两个接口可以看作是事务的描述。
PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
2.1 PlatformTransactionManager:事务管理接口
Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是:PlatformTransactionManager,它是Spring 事务结构中的核心接口。
通过这个接口,Spring 为各个平台如:JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
PlatformTransactionManager 接口的具体实现如下:
PlatformTransactionManager接口中定义了三个方法:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
// 获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
// 提交事务
void commit(TransactionStatus var1) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
通过PlatformTransactionManager的接口 可以看出,Spring 事务的 的三个核心方法,事务开启 ,提交事务,回滚事务,Spring 事务功能的实现 都是围绕这三个方法来实现。
这里多插一嘴。为什么要定义或者说抽象出来PlatformTransactionManager这个接口呢?
主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。
“为什么我们要用接口?”
《设计模式》(GOF 那本)这本书在很多年前都提到过说要基于接口而非实现编程,你真的知道为什么要基于接口编程么?
纵观开源框架和项目的源码,接口是它们不可或缺的重要组成部分。要理解为什么要用接口,首先要搞懂接口提供了什么功能。我们可以把接口理解为提供了一系列功能列表的约定,接口本身不提供功能,它只定义行为。但是谁要用,就要先实现我,遵守我的约定,然后再自己去实现我定义的要实现的功能。
举个例子,我上个项目有发送短信的需求,为此,我们定了一个接口,接口只有两个方法:
1.发送短信 2.处理发送结果的方法。
刚开始我们用的是阿里云短信服务,然后我们实现这个接口完成了一个阿里云短信的服务。后来,我们突然又换到了别的短信服务平台,我们这个时候只需要再实现这个接口即可。这样保证了我们提供给外部的行为不变。几乎不需要改变什么代码,我们就轻松完成了需求的转变,提高了代码的灵活性和可扩展性。
什么时候用接口?当你要实现的功能模块设计抽象行为的时候,比如发送短信的服务,图床的存储服务等等。
2.1.1 JDBC事务
如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会为你处理事务边界。为了使用DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
实际上,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务,而后者是通过DataSource获取到的。通过调用连接的commit()方法来提交事务,同样,事务失败则通过调用rollback()方法进行回滚。
2.1.2 Hibernate事务
如果应用程序的持久化是通过Hibernate实习的,那么你需要使用HibernateTransactionManager。对于Hibernate3,需要在Spring上下文定义中添加如下的<bean>声明:
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
sessionFactory属性需要装配一个Hibernate的session工厂,HibernateTransactionManager的实现细节是它将事务管理的职责委托给org.hibernate.Transaction对象,而后者是从Hibernate Session中获取到的。当事务成功完成时,HibernateTransactionManager将会调用Transaction对象的commit()方法,反之,将会调用rollback()方法。
2.1.3 Java持久化API事务(JPA)
Hibernate多年来一直是事实上的Java持久化标准,但是现在Java持久化API作为真正的Java持久化标准进入大家的视野。如果你计划使用JPA的话,那你需要使用Spring的JpaTransactionManager来处理事务。你需要在Spring中这样配置JpaTransactionManager:
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
JpaTransactionManager只需要装配一个JPA实体管理工厂(javax.persistence.EntityManagerFactory接口的任意实现)。JpaTransactionManager将与由工厂所产生的JPA EntityManager合作来构建事务。
2.1.4 Java原生API事务
如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用JtaTransactionManager:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName" value="java:/TransactionManager" />
</bean>
JtaTransactionManager将事务管理的责任委托给javax.transaction.UserTransaction和javax.transaction.TransactionManager对象,其中事务成功完成通过UserTransaction.commit()方法提交,事务失败通过UserTransaction.rollback()方法回滚。
2.2 TransactionInfo:事务信息
事务信息对象,包括一个事务所有的信息,持有事务管理器、事务定义对象、目标方法唯一标识、事务状态对象、外层的TransactionInfo,外层的TransactionInfo 用于在应用程序中获取 当前的TransactionInfo 对象。
2.3 TransactionDefinition:事务定义
事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务,这个方法里面的参数是 TransactionDefinition 类 ,这个类就定义了一些基本的事务属性。
什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。
事务属性包含了 5 个方面:
- 隔离级别
- 传播行为
- 回滚规则
- 是否只读
- 事务超时
TransactionDefinition 接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
// 返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
@Nullable
String getName();
}
2.4 TransactionStatus:事务状态
TransactionStatus接口用来记录事务的状态。该接口定义了一组方法,用来获取或判断事务的相应状态信息。
PlatformTransactionManager.getTransaction(…)方法返回一个 TransactionStatus 对象。
TransactionStatus 接口内容如下:
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}
2.5 TransationSynchronization:事务同步回调接口
事务同步回调接口,在事务 周期的各个点 执行回调 方法。比如 挂起 ,继续,提交前后 ,完成前后 。用于 管理 应用程序在事务周期中绑定的资源。在Spring - Mybatis 整合时,正式Mybatis 正式利用了TransationSynchronization同步器,才让Mybatis 的事务管理交给了 Spring 事务来管理。
2.6 TransactionSynchronizationManager:事务同步回调的管理器
在事务运行过程中,需要保存一些状态,比如 数据库连接,
ThreadLocal<Map<Object, Object>> resources - 应用代码随事务的声明周期绑定的对象
ThreadLocal<Set<TransactionSynchronization>> synchronizations-使用的同步器,用于应用扩展
ThreadLocal<String> currentTransactionName-事务的名称
ThreadLocal<Boolean> currentTransactionReadOnly-事务是否是只读
ThreadLocal<Integer> currentTransactionIsolationLevel-事务的隔离级别
ThreadLocal<Boolean> actualTransactionActive-是否实际的开启了事务,如果加入 到 别的事务,就不是实际开启事务。
2.7 SuspendedResourceHolder:挂起的资源持有对象
在挂起一个事务时,用于记录被挂起事务的运行时信息,这些信息就是TransactionSynchronizationManager中记录的事务信息。然后将这些信息 保存在 新的DefaultTransactionStatus对象中,便于内部事务运行结束后,恢复外层事务。
三、事务属性详解
实际业务开发中,大家一般都是使用 @Transactional 注解来开启事务,很多人并不清楚这个参数里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。
3.1 事务传播行为(事务传播机制)
事务传播行为是为了解决业务层方法之间互相调用的事务问题。事务的传播性一般用在事务嵌套 的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
举个例子:我们在 A 类的aMethod()方法中调用了 B 类的 bMethod() 方法。这个时候就涉及到业务层方法之间互相调用的事务问题。如果我们的 bMethod()发生异常需要回滚,如何配置事务传播行为才能让 aMethod()也跟着回滚呢?这个时候就需要事务传播行为的知识了,如果你不知道的话一定要好好看一下。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.xxx)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.xxx)
public void bMethod {
//do something
}
}
在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
......
}
不过,为了方便使用,Spring 相应地定义了一个枚举类:Propagation
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
public enum Propagation {
//如果当前存在事务则加入该事务 如果当前没有事务,则创建一个新的事务
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);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
正确的事务传播行为可能的值如下:
1.TransactionDefinition.PROPAGATION_REQUIRED
REQUIRED 是 Spring 默认的事务传播类型,该传播类型的特点是:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。
它是使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:
- 如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务并且被Propagation.REQUIRED修饰的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。
举个例子:如果我们上面的aMethod()和bMethod()使用的都是PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.REQUIRED)
public void bMethod {
//do something
}
}
为了验证 REQUIRED 事务传播类型的特点,我们再来做几个测试。
还是上面 methodA 和 methodB 的例子。当 methodA 不开启事务,methodB 开启事务,这时候 methodB 就是独立的事务,而 methodA 并不在事务之中。因此当 methodB 发生异常回滚时,methodA 中的内容就不会被回滚。用如下的代码就可以验证我们所说的。
public void methodA(){
System.out.println("methodA");
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional
public void methodB(){
System.out.println("methodB");
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
最终的结果是:tablea 插入了数据,tableb 没有插入数据,符合了我们的猜想。
当 methodA 开启事务,methodB 也开启事务。按照我们的结论,此时 methodB 会加入 methodA 的事务。此时,我们验证当父子事务分别回滚时,另外一个事务是否会回滚。
我们先验证第一个:当父方法事务回滚时,子方法事务是否会回滚?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
throw new RuntimeException();
}
@Transactional
public void methodB(){
tableService.insertTableB(new TableEntity());
}
结果是:talbea 和 tableb 都没有插入数据,即:父事务回滚时,子事务也回滚了。
我们继续验证第二个:当子方法事务回滚时,父方法事务是否会回滚?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional
public void methodB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
结果是:talbea 和 tableb 都没有插入数据,即:子事务回滚时,父事务也回滚了。
我们继续验证第三个:当字方法事务回滚时,父方法捕捉了异常,父方法事务是否会回滚?
@Transactional
public void methodA() {
tableService.insertTableA(new TableEntity());
try {
transactionServiceB.methodB();
} catch (Exception e) {
System.out.println("methodb occur exp.");
}
}
@Transactional
public void methodB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
结果是:talbea 和 tableb 都没有插入数据,即:子事务回滚时,父事务也回滚了。所以说,这也进一步验证了我们之前所说的:REQUIRED 传播类型,它是父子方法共用同一个事务的。
2.TransactionDefinition.PROPAGATION_REQUIRES_NEW
REQUIRES_NEW 也是常用的一个传播类型,该传播类型的特点是:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
举个例子:如果我们上面的bMethod()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,aMethod还是用PROPAGATION_REQUIRED修饰的话。如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。但是,如果 bMethod()抛出了未被捕获的异常(如果在bMethod方法内try...catch处理了这个异常,那么aMethod就不会检测到这个异常,也就不会回滚了)并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚,因为这个异常被 aMethod()的事务管理机制检测到了。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
为了验证 REQUIRES_NEW 事务传播类型的特点,我们再来做几个测试。
首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
tableService.insertTableB(new TableEntity());
}
结果是:tablea 没有插入数据,tableb 插入了数据,即:父方法事务回滚了,但子方法事务没回滚。这可以证明父子方法的事务是独立的,不相互影响。
下面,我们来看看:当子方法事务发生异常时,父方法事务是否会回滚?
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
结果是:tablea 没有插入了数据,tableb 没有插入数据。
从这个结果来看,貌似是子方法事务回滚,导致父方法事务也回滚了。但我们不是说父子事务都是独立的,不会相互影响么?怎么结果与此相反呢?
其实是因为子方法抛出了异常,而父方法并没有做异常捕捉,此时父方法同时也抛出异常了,于是 Spring 就会将父方法事务也回滚了。如果我们在父方法中捕捉异常,那么父方法的事务就不会回滚了,修改之后的代码如下所示。
@Transactional
public void methodA(){
tableService.insertTableA(new TableEntity());
// 捕捉异常
try {
transactionServiceB.methodB();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}
结果是:tablea 插入了数据,tableb 没有插入数据。这正符合我们刚刚所说的:父子事务是独立的,并不会相互影响。
这其实就是我们上面所说的:父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。因为如果执行过程中发生 RuntimeException 异常和 Error 的话,那么 Spring 事务是会自动回滚的。
3.TransactionDefinition.PROPAGATION_NESTED
NESTED 也是常用的一个传播类型,该方法的特性与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。
可以看到 NESTED 与 REQUIRED 的区别在于:父方法与子方法对于共用事务的描述是不一样的,REQUIRED 说的是共用同一个事务,而 NESTED 说的是在嵌套事务执行。这一个区别的具体体现是:在子方法事务发生异常回滚时,父方法有着不同的反应动作。
对于 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。
如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED类似的操作。也就是说:
- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
- 如果外部方法无事务,则单独开启一个事务,与 PROPAGATION_REQUIRED 类似。
这里还是简单举个例子:如果 bMethod() 回滚的话,aMethod()不会回滚(内部事务回滚,外部事务不回滚)。如果 aMethod() 回滚的话,bMethod()会回滚(外部事务回滚,内部事务也回滚)。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.NESTED)
public void bMethod {
//do something
}
}
为了验证 NESTED 事务传播类型的特点,我们再来做几个测试。
首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?
@TransactionalpublicvoidmethodA() {
tableService.insertTableA(newTableEntity());
transactionServiceB.methodB();
thrownewRuntimeException();
}
@Transactional(propagation = Propagation.NESTED)
publicvoidmethodB() {
tableService.insertTableB(newTableEntity());
}
结果是:tablea 和 tableb 都没有插入数据,即:父子方法事务都回滚了。这说明父方法发送异常时,子方法事务会回滚。
接着,我们继续验证一下:当子方法事务发生异常时,如果父方法没有捕捉异常,父方法事务是否会回滚?
@TransactionalpublicvoidmethodA() {
tableService.insertTableA(newTableEntity());
transactionServiceB.methodB();
}
@Transactional(propagation = Propagation.NESTED)
publicvoidmethodB() {
tableService.insertTableB(newTableEntity());
thrownewRuntimeException();
}
结果是:tablea 和 tableb 都没有插入数据,即:父子方法事务都回滚了。这说明子方法发送异常回滚时,如果父方法没有捕捉异常(try...catch异常),那么父方法事务也会回滚。
最后,我们验证一下:当子方法事务发生异常时,如果父方法捕捉了异常,父方法事务是否会回滚?
@TransactionalpublicvoidmethodA() {
tableService.insertTableA(newTableEntity());
try{
transactionServiceB.methodB();
} catch(Exceptione) {
}
}
@Transactional(propagation = Propagation.NESTED)
publicvoidmethodB() {
tableService.insertTableB(newTableEntity());
thrownewRuntimeException();
}
结果是:tablea 插入了数据,tableb 没有插入数据,即:父方法事务没有回滚,子方法事务回滚了。这说明子方法发送异常回滚时,如果父方法捕捉了异常,那么父方法事务就不会回滚。
看到这里,相信大家已经对 REQUIRED、REQUIRES_NEW 和 NESTED 这三个最常用的事务传播类型有了深入的理解了。最后,让我们来总结一下:
事务传播类型 | 特性 |
REQUIRED | 当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。 |
REQUIRES_NEW | 无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。 |
NESTED | 当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。 |
4.TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
这个使用的很少,就不举例子来说了。
若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚,这里不对照案例讲解了,使用的很少。
- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
3.2 事务隔离级别
事务的隔离级别定义一个事务可能受其他并发务活动活动影响的程度,可以把事务的隔离级别想象为这个事务对于事物处理数据的自私程度。
在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致以下问题:
- 脏读(Dirty read)
脏读发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的。
- 不可重复读(Nonrepeatable read)
不可重复读发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。不可重复读重点在修改。
- 幻读(Phantom reads)
幻读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。幻读重点在新增或删除。
在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常涉及到锁定在数据库中的记录(甚至有时是锁表)。完全隔离要求事务相互等待来完成工作,会阻碍并发。因此,可以根据业务场景选择不同的隔离级别。
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
public interface TransactionDefinition {
......
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
......
}
和事务传播行为那块一样,为了方便使用,Spring 也相应地定义了一个枚举类:Isolation
public enum Isolation {
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
// 最低级别,只能保证不读取 物理上损害的数据,允许脏读
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
// 只能读到已经提交的数据
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
// 可重复读
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
// 串行化读,读写相互阻塞
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
private final int value;
Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
下面我依次对每一种事务隔离级别进行介绍:
- TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
事务的隔离级别示例:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
读取未提交数据(会出现脏读, 不可重复读) 基本不使用
3.3 事务超时属性
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的一个特性就是它的超时。
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。
假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
事务的超时性示例:
@Transactional(timeout=30)
3.4 事务只读属性
如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
......
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
}
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?
拿 MySQL 的 innodb 举例子,根据官网 MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.2 autocommit, Commit, and Rollbackopen in new window描述:MySQL 默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到 MySQL 服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
但是,如果你给方法加上了Transactional注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。
如果不加Transactional,每条sql会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
分享一下关于事务只读属性,其他人的解答:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
只读示例:
@Transactional(readOnly=true)
该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。
3.5 事务回滚规则
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行时异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常何IO异常(IOException)时不会回滚。
例如下面的代码执行后,tablea 和 tableb 两个表格,都会插入一条数据,它们是不会回滚的:
@Transactional
public void methodA() throws Exception {
tableService.insertTableA(new TableEntity());
transactionServiceB.methodB();
}
@Transactional
public void methodB() throws Exception {
tableService.insertTableB(new TableEntity());
// 非 RuntimeException
throw new Exception();
}
回滚示例:
指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。
如果你想要回滚你定义的特定的异常类型的话,可以通过注解设置:
@Transactional(rollbackFor= MyException.class)
也可以通过XML设置:
<tx:method name="update" propagation="REQUIRED"/>
但是比如设置了IO异常就回滚,如果在代码中try...catch处理了这个异常之后,就不会回滚了。
四、Spring事务使用方法论
看完了事务的传播类型,我们对 Spring 事务又有了深刻的理解。
看到这里,你应该也明白:使用事务,不再是简单地使用 @Transaction 注解就可以,还需要根据业务场景,选择合适的传播类型。那么我们再升华一下使用 Spring 事务的方法论。一般来说,使用 Spring 事务的步骤为:
- 根据业务场景,分析要达成的事务效果,确定使用的事务传播类型。
- 在 Service 层使用 @Transaction 注解,配置对应的 propogation 属性。(事务管理一般是对Service层使用的,所以以后编写Service层时一定不要忘了加事务管理,事务管理是系统开发的最基本素质,如果一个程序员在开发的时候不去对Service层做事务管理,那么这个程序员就该被开除了)
下次遇到要使用事务的情况,记得按照这样的步骤去做哦~
@Transactional 介绍
@Transactional 注解 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该注解来覆盖类级别的定义。
虽然@Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
相关文章:
【Spring框架】Spring事务的介绍与使用方法
⚠️ 再提醒一次:Spring 本身并不实现事务,Spring事务 的本质还是底层数据库对事务的支持。你的程序是否支持事务首先取决于数据库 ,比如使用 MySQL 的话,如果你选择的是 innodb 引擎,那么恭喜你,是可以支持…...
七夕特别篇 | 浪漫的Bug
文章目录 前言一、迷失的爱情漩涡(多线程中的错误同步)1.1 Bug 背景1.2 Bug 分析1.3 Bug 解决 二、心形积分之恋(心形面积计算中的数值积分误差)1.1 Bug 背景1.1.1 背景1.1.2 数学模型 1.2 Bug 分析1.2.1 初始代码1.2.2 代码工作流…...
数据结构双向链表
Hello,好久不见,今天我们讲链表的双向链表,这是一个很厉害的链表,带头双向且循环,学了这个链表,你会发现顺序表的头插头删不再是一个麻烦问题,单链表的尾插尾删也变得简单起来了,那废…...
解决政务审计大数据传输难题!镭速传输为政务行业提供解决方案
政务行业是国家治理的重要组成部分,涉及到国家安全、社会稳定、民生福祉等方面。随着信息技术的快速发展和革新,政务信息化也迎来了新一轮的升级浪潮。国家相继出台了《国家信息化发展战略纲要》《“十三五”国家信息化规划》《“十四五”推进国家政务信…...
redis 7高级篇1 redis的单线程与多线程
一 redis单线程与多线程 1.1 redis单线程&多线程 1.redis的单线程 redis单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理…...
GO语言:Worker Pools线程池、Select语句、Metex互斥锁详细示例教程
目录标题 一、Buffered Channels and Worker Pools1. Goroutine and Channel Example 线程和通道示例2. Deadlock 死锁3. Closing buffered channels 关闭通道4. Length vs Capacity 长度和容量5. WaitGroup6. Worker Pool Implementation 线程池 二、Select1. Example2. Defau…...
vue ui 创建项目没有反应
问题 cmd中输入 vue ui 没有反应 解决办法 vue ui命令需要vue3.0以上的版本才可以 1、查看当前版本 vue --version vue版本在3.0以下是没有ui命令的 2、查看版本所拥有的命令 vue -h 3、卸载之前版本的vue npm uninstall vue-cli -g 卸载完成,检查是否已经…...
go语言中channel类型
目录 一、什么是channel 二、为什么要有channel 三、channel操作使用 初始化 操作 单向channel 双向channel,可读可写 四、close下什么场景会出现panic 五、总结 一、什么是channel Channels are a typed conduit through which you can send and receive …...
基于STM32F1的电子罗盘HMC5883L角度测量
基于STM32F1的电子罗盘HMC5883L角度测量 参考 1. HMC5883L模块 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Axqqv48y-1692885921487)(…\img\HMC5883L.png)] 型号:GY-271使用芯片:HMCL5883L供电电源:3-5V通…...
Oracle解锁表、包、用户、杀会话、停job
Oracle解锁表、包、用户、杀会话、停job 一、创建包tzq_server_pkg二、授权给需要使用的用户log三、解锁表:执行存过unlock_table(schema_name, table_name)四、解锁包:执行存过unlock_package(schema_name, pkg_name)五、解锁用户:执行存过u…...
软考高级系统架构设计师系列论文九十九:论软件开发平台的选择和应用
软考高级系统架构设计师系列论文九十九:论软件开发平台的选择和应用 一、相关知识点二、摘要三、正文四、总结一、相关知识点 软考高级系统架构设计师系列之:面向构件的软件设计,构件平台与典型架构二、摘要 本文从一个行业MIS系统的开发实践,讨论了软件开发平台的选择和应…...
Redis Pub/Sub 指南
Redis 不仅仅是一个数据库,还可以作为支持发布和订阅(Pub/Sub)操作的消息代理。本文将使用 Navicat for Redis 简要概述 Redis 的 Pub/Sub 功能。 关于发布或订阅消息范式 Pub/Sub 是一种模式,发送者(广播者…...
Nest(2):Nest 应用目录结构和脚手架命令介绍
Nest 应用目录结构和脚手架命令介绍 在正式使用 NestJS 进行开发之前,先来了解下 Nest 应用的目录结构,和一些常用的脚本命令。 工程目录 下面是使用 nest/cli 创建的 Nest 项目的目录结构。 上篇文章中介绍了 src 目录以及目录下各个文件的作用。下面…...
【嵌入式】MKV31F512VLL12 微控制器 (MCU) 、Cyclone® IV E EP4CE10E22I8LN,FPGA-现场可编程门阵列芯片
1、MKV31F512VLL12 微控制器 (MCU) 是适用于BLDC、PMSM和ACIM电机控制应用的高性能解决方案。这些MCU采用运行频率为100MHz/120MHz、带数字信号处理 (DSP) 和浮点单元 (FPU) 的ARM Cortex-M4内核。KV3x MCU配备两个采样率高达1.2MS/s的16位ADC、多个控制定时器以及512KB闪存。 …...
矢量调制分析基础
前言 本文介绍VSA 的矢量调制分析和数字调制分析测量能力。某些扫频调谐频谱分析仪也能通过使用另外的数字无线专用软件来提供数字调制分析。然而,VSA 通常在调制格式和解调算法配置等方面提供更大的测量灵活性,并提供更多的数据结果和轨迹轨迹显示。本…...
ensp-Ipv6配置配置
ensp-Ipv6配置配置 📎ipv6.zip📎Ipv6 网络.docx...
java八股文面试[java基础]—— hashCode 与 equals 区别 == 与 equals的区别
两个对象的hashCode()相同时,equals()相等吗?_两个对象的hashcode一样,equal一样么_不想当个程序员的博客-CSDN博客 equals():比较的是非基本类型的数据的引用地址(即内存地址)是否相同,但是对于重写equal…...
Dubbo之PojoUtils源码分析
功能概述 PojoUtils是一个工具类,能够进行深度遍历,将简单类型与复杂类型的对象进行转换,在泛化调用时用到(在泛化调用中,主要将Pojo对象与Map对象进行相互转换) 功能分析 核心类PojoUtils分析 主要成员…...
【C++】—— C++11新特性之 “右值引用和移动语义”
前言: 本期,我们将要的介绍有关 C右值引用 的相关知识。对于本期知识内容,大家是必须要能够掌握的,在面试中是属于重点考察对象。 目录 (一)左值引用和右值引用 1、什么是左值?什么是左值引用…...
谈一谈redis脑裂
什么是redis脑裂 (1)一主多从架构中,主节点与客户端通信正常,主节点与哨兵、从节点连接异常,客户端仍正常写入数据 (2)哨兵判定主节点下线,重新选主 (3)原主…...
基于原生Servlet使用模板引擎Thymeleaf访问界面
我们常在Spring Boot项目中使用Thymeleaf模板引擎,今天突发奇想,尝试原生Servlet访问! 说做就做 搭建完整的WEB项目 其中的大部分依赖都是后续报错 追加进来的 导入依赖 thymeleaf-3.0.11.RELEASE.jar 第一次访问 访问地址: http://localhost:8080…...
【C语言】15-函数-1
1. 初步认识函数 通过前几章的学习,已经可以编写一些简单的 C 语言程序了,但是如果程序的功能比较多,规模比较大,把所有的程序代码都写在一个主函数(main函数)中,就会使主函数变得庞杂、头绪不清,使阅读和维护程序变得困难。此外,有时程序中要多次实现某一功能就需要…...
08-信息收集-架构、搭建、WAF等
信息收集-架构、搭建、WAF等 信息收集-架构、搭建、WAF等一、前言说明二、CMS识别技术三、源码获取技术四、架构信息获取技术五、站点搭建分析1、搭建习惯-目录型站点2、搭建习惯-端口类站点3、搭建习惯-子域名站点4、搭建习惯-类似域名站点5、搭建习惯-旁注,c段站点…...
Qt --- 显示相关设置 窗口属性等
主界面,窗口 最小化 最大化 关闭按钮、显示状态自定义: setWindowFlags(Qt::CustomizeWindowHint); setWindowFlags(Qt::WindowCloseButtonHint); //只要关闭按钮 setWindowFlags(Qt::WindowFlags type) Qt::FrameWindowHint:没有边框的窗口 Qt::Window…...
使用小程序实现左侧菜单,右侧列表双向联动效果
目录 引言理解双向联动效果的重要性scrollview属性介绍实现左侧菜单数据准备渲染菜单列表监听菜单点击事件实现右侧列表数据结构设计初始数据渲染监听列表滚动事件左侧菜单与右侧列表联动获取当前滚动位置计算对应菜单项联动效果优化用户体验考虑平滑滚动效果菜单高亮状态...
selenium中处理验证码问题
验证码 基本作用:可以实现当前访问页面的数据安全性、还可以减少用户的并发数; 类型:1、纯数字、纯字母;2、汉字组合;3、数学运算题;4、滑动;5、图片(选不同的、选相同、成语顺序&…...
EMR电子病历系统 SaaS电子病历编辑器源码 电子病历模板编辑器
EMR(Electronic Medical Record)指的是电子病历。它是一种基于电子文档的个人医疗记录,可以包括病人的病史、诊断、治疗方案、药物处方、检查报告和护理计划等信息。EMR采用计算机化的方式来存储、管理和共享这些信息,以便医生和医…...
一些自定义hooks
文章目录 1、点击框外隐藏弹窗hook 1、点击框外隐藏弹窗hook **描述:**有一个需要自己封装弹窗的组件,实现点击弹窗框外时隐藏弹窗 代码: import { useEffect } from “react”; // 点击框外hooks import { useEffect } from "react&q…...
基于Citespace、vosviewer、R语言的文献计量学可视化分析技术及全流程文献可视化SCI论文高效写作方法
文献计量学是指用数学和统计学的方法,定量地分析一切知识载体的交叉科学。它是集数学、统计学、文献学为一体,注重量化的综合性知识体系。特别是,信息可视化技术手段和方法的运用,可直观的展示主题的研究发展历程、研究现状、研究…...
lEC 61068-2-14_2023环境试验.第2-14部分:试验.试验N:温度变化, 最新版发布
https://download.csdn.net/download/m0_67373485/88251313 lEC 61068-2-14_2023环境试验.第2-14部分:试验.试验N:温度变化 A change of temperature test is intended to determine the effect on the specimen of a changeof temperature or a succession of changes of tem…...
怎样破解网站后台密码/上海网站制作开发
举个STM32 HAL库里的结构体的例子: b被覆盖 这里的函数指针,巧妙的一逼!!!好好想想,好好看看!!!...
网站建设的发展/长沙电商优化
是否是美国金融业监管机构( FINRA ) /美国证券投资人保护组织( SIPC )的资深会员,是评价一个美股券商是否正规有实力的最为直接的方式。 成为这两大机构的会员后,即使该会员券商破产,投资者的股…...
中小型企业查询/汕头seo计费管理
别计算什么大小了,任何的网页都是最终解析为html了,这样子的话你可以尝试正则表达式去匹配查找。你只需要按照你要找的标签去把内容给捕捉到就行了。这是我做过的一个例子,具体的匹配你可以看看正则表达式的相关内容。public String getAll(S…...
专门做电容的网站/百度seo关键词排名
点击上方“蓝色字”可关注我们!暴走时评:一种名为Beam的新加密货币于今日首次亮相,该币是两种使用所谓“mimblewimble”协议,备受期待的隐私币之一。mimblewimble协议宣称可以使交易保密且几乎无法追踪,最初出现在2016…...
做网站标题代码/上海排名优化seobwyseo
clamp函数用于约束返回值到A和B之间,若value小于min,则返回min;若value大于max,则返回max,起到上下截断的作用。 以下内容转载自:https://blog.csdn.net/u013230189/article/details/82627375,…...
梅州市工程建设交易中心网站/网站策划运营
打开背包,打开商店,里面的东西琳琅满目,怎么实现呢?直接通过UI加载?是不是太消耗内存啦,此时我们就读表加载 商品表 Item表 从上面shop表中读取序列号,在Item表中进行查找加载 c#读表的名字要与表的名字一致哟(其实也完全不一样,人家前面多个Dr嘛,哈哈) 是不是在哪见过?对,就…...