Spring事务
1.了解事务
1.1 事务是什么
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性一个事务中的一系列的操作要么全部成功,要么一个都不做。
1.2 事务的四大特性
数据库事务 transanction 正确执行的四个基本要素。ACID,原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability)。
1.原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
2.一致性(Correspondence):在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
3.隔离性(Isolation):隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行 相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆, 必须串行化或序列化请 求,使得在同一时间仅有一个请求用于同一数据。
4.持久性(Durability):在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
1.3 注意事项
- 事务只能应用到 public 方法上才会有效。
- 事务需要从外部调用,Spring 自调事务用会失效。
Spring中的事务
2.1 核心接口
PlatformTransactionManager
Spring
事务管理器的接口是org.springframework.transaction.PlatformTransactionManager
,通过这个接口,Spring为各个平台如JDBC
、Hibernate
等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
Public interface PlatformTransactionManager() {
// 由TransactionDefinition得到TransactionStatus对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
Void commit(TransactionStatus status) throws TransactionException;
// 回滚
Void rollback(TransactionStatus status) throws TransactionException;
}
}
从这里可知具体的具体的事务管理机制对Spring来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以Spring事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。
TransactionDefinition
上面讲到的事务管理器接口PlatformTransactionManager
通过getTransaction(TransactionDefinition definition)
方法来得到事务,这个方法里面的参数是TransactionDefinition
类,这个类就定义了一些基本的事务属性。
那么什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面,如图所示:
public interface TransactionDefinition {
int getPropagationBehavior(); // 返回事务的传播行为
int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getTimeout(); // 返回事务必须在多少秒内完成
boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}
如果开发者使用了编程式事务的话,直接使用 DefaultTransactionDefinition
即可。
TransactionStatus
TransactionStatus 可以直接理解为事务本身,该接口源码如下:
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction(); // 方法获取当前事务是否是一个新事务。
boolean hasSavepoint(); // 方法判断是否存在 savePoint()。
void setRollbackOnly(); // 方法设置事务必须回滚。
boolean isRollbackOnly(); // 方法获取事务只能回滚。
void flush(); // 方法将底层会话中的修改刷新到数据库,一般用于 Hibernate/JPA 的会话,对如 JDBC 类型的事务无任何影响。
boolean isCompleted(); //方法用来获取是一个事务是否结束。
}
2. 编程式事务
2.1 编程式和声明式事务的区别
Spring提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。
简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。
2.2 如何实现编程式事务?
Spring提供两种方式的编程式事务管理,分别是:使用TransactionTemplate和直接使用PlatformTransactionManager。下面看代码演示;
2.3 使用transactionManager
首先新建一个maven项目,引入依赖 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring_transaction</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
</project>
连接数据库,建表
配置文件applicationContext
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.hong.demo"></context:component-scan>
<!--
1.配置数据源
-->
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="url" value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC"></property>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
</bean>
<!--
2.提供一个事务管理器
-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
3.配置 TransactionTemplate
-->
<bean class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"></property>
</bean>
<!--
4.配置 JdbcTemplate
-->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
UserService
package com.hong.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Component
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
PlatformTransactionManager transactionManager;
@Autowired
TransactionTemplate transactionTemplate;
public void transfer(){
// 1.定义默认的事务属性
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// 2.获取TransactionStatus
TransactionStatus status = transactionManager.getTransaction(definition);
try {
jdbcTemplate.update("update account set money = ? where name = ?",500,"tom");
// int i = 1/0;
// 提交事务
transactionManager.commit(status);
} catch (DataAccessException e) {
e.printStackTrace();
// 回滚事务
transactionManager.rollback(status);
}
}
}
UserDemo
package com.hong.demo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class UserDemo {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean(UserService.class);
userService.transfer();
}
}
如果我们需要配置事务的隔离性、传播性等,可以在DefaultTransactionDefinition
对象中进行配置。
2.3 使用TransactionTemplate
public void transfer2() {
// 执行成功自动提交
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
jdbcTemplate.update("update account set money = ? where name = ?",500,"tom");
// int i = 1/0;
} catch (DataAccessException e) {
// 设置当前事务回滚
status.setRollbackOnly();
e.printStackTrace();
}
}
});
}
直接注入 TransactionTemplate
,然后在 execute
方法中添加回调写核心的业务即可,当抛出异常时,将当前事务标注为只能回滚即可。注意,execute
方法中,如果不需要获取事务执行的结果,则直接使用 TransactionCallbackWithoutResult
类即可,如果要获取事务执行结果,则使用 TransactionCallback
即可。
编程式事务由于代码入侵太严重了,因为在实际开发中使用的很少,我们在项目中更多的是使用声明式事务。
3.声明性事务
声明式事务如果使用 XML
配置,可以做到无侵入;如果使用 Java
配置,也只有一个 @Transactional
注解侵入而已,相对来说非常容易。
3.1 XML配置
1.配置事务管理器
<!--
1.配置数据源
-->
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="url" value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC"></property>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
</bean>
<!--
2.提供一个事务管理器
-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
2.配置事务通知
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--xml文件配置隔离性-->
<tx:method name="add*" isolation="READ_UNCOMMITTED"></tx:method>
<tx:method name="insert*"></tx:method>
<tx:method name="delete*"></tx:method>
<tx:method name="update*"></tx:method>
<tx:method name="transfer*"></tx:method>
</tx:attributes>
</tx:advice>
3.配置AOP
<aop:config>
<aop:pointcut id="pc1" expression="execution(* com.hong.demo.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>
UserService
public void transfer() {
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
}
UserDemo
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean(UserService.class);
userService.transfer();
}
3.2 用Java配置
新建一个Java配置类
JavaConfig
package com.hong.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackages = "com.hong.demo")
// 开启事务的注解zhichi
@EnableTransactionManagement
public class JavaConfig {
@Bean
DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUsername("root");
ds.setPassword("123456");
ds.setUrl("jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC");
return ds;
}
@Bean
JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
这里要配置的东西其实和 XML 中配置的都差不多,最最关键的就两个:
- 事务管理器 PlatformTransactionManager。
- @EnableTransactionManagement 注解开启事务支持。
配置完成后,接下来,哪个方法需要事务就在哪个方法上添加 @Transactional
注解即可,向下面这样:
@Transactional
public void transfer4(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
}
3.3 混合配置
用Java代码和XML混合配置来实现
applicationContext.xml
<!--开启事务的注解支持-->
<tx:annotation-driven></tx:annotation-driven>
JavaConfig
package com.hong.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackages = "com.hong.demo")
@ImportResource(locations = "classpath:applicationContext.xml")
public class JavaConfig2 {
@Bean
DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUsername("root");
ds.setPassword("123456");
ds.setUrl("jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC");
return ds;
}
@Bean
JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
Java 配置中通过 @ImportResource 注解导入了 XML 配置,XML 配置中的内容就是开启 @Transactional
注解的支持,所以 Java 配置中省略了 @EnableTransactionManagement 注解。
4.事务的属性及实例
4.1 隔离性
MySQL 中有四种不同的隔离级别,这四种不同的隔离级别在 Spring 中都得到了很好的支持。Spring 中默认的事务隔离级别是 default,即数据库本身的隔离级别是啥就是啥,default 就能满足我们日常开发中的大部分场景。
不过如果项目有需要,我们也可以调整事务的隔离级别。 PlatformTransactionManager
// 编程式事务配置
definition.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
transactionTemplate
// 编程式事务配置
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
声明式XML上
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--xml文件配置隔离性-->
<tx:method name="add*" isolation="READ_UNCOMMITTED"></tx:method>
<tx:method name="insert*"></tx:method>
<tx:method name="delete*"></tx:method>
<tx:method name="update*"></tx:method>
<tx:method name="transfer*"></tx:method>
</tx:attributes>
</tx:advice>
注解上
// 注解配置隔离级别
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer5() {
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
}
4.2 传播性
所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition
中定义,具体常量的解释见下:
- Propagation.REQUIRED(required):支持当前事务,如果当前有事务, 那么加入事务, 如果当前没有事务则新建一个(默认情况)
- Propagation.NOT_SUPPORTED(not_supported) : 以非事务方式执行操作,如果当前存在事务就把当前事务挂起,执行完后恢复事务(忽略当前事务);
- Propagation.SUPPORTS (supports) :如果当前有事务则加入,如果没有则不用事务。
- Propagation.MANDATORY (mandatory) :支持当前事务,如果当前没有事务,则抛出异常。(当前必须有事务)
- PROPAGATION_NEVER (never) :以非事务方式执行,如果当前存在事务,则抛出异常。(当前必须不能有事务)
- Propagation.REQUIRES_NEW (requires_new) :支持当前事务,如果当前有事务,则挂起当前事务,然后新创建一个事务,如果当前没有事务,则自己创建一个事务。
- Propagation.NESTED (nested 嵌套事务) :如果当前存在事务,则嵌套在当前事务中。如果当前没有事务,则新建一个事务自己执行(和required一样)。嵌套的事务使用保存点作为回滚点,当内部事务回滚时不会影响外部事物的提交;但是外部回滚会把内部事务一起回滚回去。(这个和新建一个事务的区别)
TransactionTemplate
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
PlatformTransactionManager
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
声明式事务的配置(XML) 声明式事务的配置(Java)
4.2.1 REQUIRED
支持当前事务,如果当前有事务, 那么加入事务, 如果当前没有事务则新建一个(默认情况) UserService2
@Service
public class UserService2 {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void update(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
}
}
UserService
@Service
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
UserService2 userService2;
public void tranfer(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
userService2.update();
}
}
- 如果 tranfer 方法本身是有事务的,则 update 方法就会加入到 tranfer 方法所在的事务中,这样两个方法将处于同一个事务中,一起成功或者一起失败(不管是 tranfer 还是 update 谁抛异常,都会导致整体回滚)。
- 如果 tranfer 方法本身是没有事务的,则 update 方法就会自己开启一个新的事务,自己玩。 如果想更清晰的看清实现过程,我们不妨开启日志功能
logging.level.root=debug
4.2.2 REQUIRES_NEW
REQUIRES_NEW 表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。换言之,不管外部方法是否有事务,REQUIRES_NEW 都会开启自己的事务。
在 REQUIRES_NEW 中可能会同时存在两个事务,外部方法的事务被挂起,内部方法的事务独自运行,而在 REQUIRED 中则不会出现这种情况,如果内外部方法传播性都是 REQUIRED,那么最终也只是一个事务。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void update(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void tranfer(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
userService2.update();
}
我们测试的时候,由于是两个更新 SQL,如果更新的查询字段不是索引字段,那么 InnoDB 将使用表锁,这样就会发生死锁(tranfor 方法执行时开启表锁,导致 update 方法陷入等待中,而必须 update 方法执行完,handle2 才能释放锁)。所以,在上面的测试中,我们要将 name 字段设置为索引字段,这样默认就使用行锁了。
4.2.3 NESTED
如果当前存在事务,则嵌套在当前事务中。如果当前没有事务,则新建一个事务自己执行(和required一样)。嵌套的事务使用保存点作为回滚点,当内部事务回滚时不会影响外部事物的提交;但是外部回滚会把内部事务一起回滚回去。(这个和新建一个事务的区别)
NESTED 修饰的内部方法(update)属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务(需要处理掉内部子事务的异常)。
@Transactional(propagation = Propagation.NESTED)
public void update(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
int i = 1/0;
}
4.2.4 MANDATORY
支持当前事务,如果当前没有事务,则抛出异常。(当前必须有事务)
public void tranfer(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
userService2.update();
}
public void update(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
// int i = 1/0;
}
4.2.5 SUPPORTS
SUPPORTS 表示如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
4.2.6 NOT_SUPPORTED
NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。
@Transactional
public void tranfer(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
userService2.update();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void update(){
jdbcTemplate.update("update account set money = ? where name = ?", 500, "tom");
// int i = 1/0;
}
4.2.7 NEVER
NEVER 表示以非事务方式运行,如果当前存在事务,则抛出异常。
4.3 回滚规则
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)以及 Error 时才会回滚,在遇到检查型(Checked Exception)异常时不会回滚。
像 1/0,空指针这些是 RuntimeException,而 IOException 则算是 Checked Exception,换言之,默认情况下,如果发生 IOException 并不会导致事务回滚。
如果我们希望发生 IOException 时也能触发事务回滚,那么可以按照如下方式配置: java
@Transactional(rollbackFor = IOException.class)
xml
<tx:advice transaction-manager="transactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="update" rollback-for="java.io.IOException"/>
</tx:attributes>
</tx:advice>
我们也可以指定在发生某些异常时不回滚,例如当系统抛出 ArithmeticException 异常并不要触发事务回滚,配置方式如下
@Transactional(noRollbackFor = ArithmeticException.class)
4.4 是否只读
只读事务一般设置在查询方法上,但不是所有的查询方法都需要只读事务,要看具体情况。
一般来说,如果这个业务方法只有一个查询 SQL,那么就没必要添加事务,强行添加最终效果适得其反。
但是如果一个业务方法中有多个查询 SQL,情况就不一样了:多个查询 SQL,默认情况下,每个查询 SQL 都会开启一个独立的事务,这样,如果有并发操作修改了数据,那么多个查询 SQL 就会查到不一样的数据。此时,如果我们开启事务,并设置为只读事务,那么多个查询 SQL 将被置于同一个事务中,多条相同的 SQL 在该事务中执行将会获取到相同的查询结果。 java
@Transactional(readOnly = true)
4.5 超时时间
超时时间是说一个事务允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
事务超时时间配置方式如下(单位为秒):
@Transactional(timeout = 300)