万字长文深入理解Spring事务管理:配置、实现与最佳实践

万字长文深入理解Spring事务管理:配置、实现与最佳实践

针对我们开发的系统,事务可以分为 单机版事务 和 分布式事务,本篇文章我们单单分析了解的是单机版事务!

什么是事务?

事务是逻辑上的一组操作,要么都执行,要么都不执行。这是保证业务操作完整性的一种数据库机制,注意这里是数据库的机制!

相信大家应该都能背上面这句话了,下面我结合我们日常的真实开发来谈一谈。

我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 savePerson() 方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。

public void savePerson() {

personDao.save(person);

personDetailDao.save(personDetail);

}

另外,需要格外注意的是:事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的 innodb 引擎。但是,如果把数据库引擎变为 myisam,那么程序也就不再支持事务了!

事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:

将小明的余额减少 1000 元。将小红的余额增加 1000 元。

万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

事务的特性(ACID)

原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

🌈 这里要额外补充一点:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!

Spring对事务的支持

Spring 对于 事务的支持底层是通过 AOP 来实现的。

这里再多提一下一个非常重要的知识点:MySQL 怎么保证原子性的?

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚之前未完成的事务。

对于单机版事务,ORM 框架对事务的支持都是通过数据库的连接(Connection)来实现的,无论我们用的是 JDBC、JDBCTemplate、Hiberate、Mybatis 他们的底层都是基于 Connection 去做的各种封装,例如 Hiberate 封装了 Transaction 对象,MyBatis 封装了 SqlSession 对象。但本质上底层最终都会调用下面这段代码:

// 关闭自动提交,开启事务

connection.setAutoCommit(false);

// 正常完成提交事务

connection.commit();

// 遇到异常的时候回滚

connection.rollback();

那么在 JavaEE 分层开发中,当接收同一个请求的时候,Service 和 DAO 是如何保证共用同一个连接(Connection)的呢?答案就是:ThreadLocal,后面我们在追源码的时候就可以看到。

Spring控制事务的编程方式

编程式事务

通过 TransactionTemplate或者TransactionManager手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。

使用TransactionTemplate 进行编程式事务管理的示例代码如下:

@Autowired

private TransactionTemplate transactionTemplate;

public void testTransaction() {

transactionTemplate.execute(new TransactionCallbackWithoutResult() {

@Override

protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

try {

// 业务代码

} catch (Exception e){

// 回滚

transactionStatus.setRollbackOnly();

}

}

});

}

使用 TransactionManager 进行编程式事务管理的示例代码如下:

@Autowired

private PlatformTransactionManager transactionManager;

public void testTransaction() {

TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

try {

// 业务代码

transactionManager.commit(status);

} catch (Exception e) {

transactionManager.rollback(status);

}

}

声明式事务

推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)。

使用 @Transactional注解进行事务管理的示例代码如下:

@Transactional

public void aMethod {

// 业务代码

}

基于XML的声明式事务

applicationContext.xml 配置文件

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"

xmlns:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

Java 相关类

public class Teacher {

private Integer id;

private String code;

private String name;

private Integer age;

private Integer sex;

// ...

}

public interface TeacherDao {

void insert(Teacher teacher);

}

@Repository

public class TeacherDaoImpl implements TeacherDao {

@Autowired

private JdbcTemplate jdbcTemplate;

@Override

public void insert(Teacher teacher) {

String insertSql = "insert into t_teacher(code, name, age, sex) values(?, ?, ?, ?)";

jdbcTemplate.update(insertSql, teacher.getCode(), teacher.getName(), teacher.getAge(), teacher.getSex());

}

}

@Service

public class TeacherService {

@Autowired

private TeacherDao teacherDao;

public void addTeacher(Teacher teacher) {

teacherDao.insert(teacher);

}

}

测试程序

public class Main {

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("transaction-xml.xml");

@Test

public void test() {

TeacherService teacherService = applicationContext.getBean(TeacherService.class);

Teacher teacher = new Teacher();

teacher.setCode("1001");

teacher.setName("张三");

teacher.setSex(0);

teacher.setAge(23);

teacherService.addTeacher(teacher);

}

}

基于注解的声明式事务

配置类

@Configuration

@EnableTransactionManagement

@PropertySource("classpath:db.properties")

@ComponentScan("world.xuewei.transaction.annotation")

public class AppConfig {

@Value("${jdbc.driveName}")

private String driveName;

@Value("${jdbc.url}")

private String url;

@Value("${jdbc.username}")

private String username;

@Value("${jdbc.password}")

private String password;

@Bean

public DataSource dataSource() {

DruidDataSource dataSource = new DruidDataSource();

dataSource.setDriverClassName(driveName);

dataSource.setUrl(url);

dataSource.setUsername(username);

dataSource.setPassword(password);

return dataSource;

}

@Bean

public JdbcTemplate jdbcTemplate(DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

@Bean

public PlatformTransactionManager transactionManager() {

DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();

transactionManager.setDataSource(dataSource());

return transactionManager;

}

}

Java 相关类

public class Leader {

private Integer id;

private String code;

private String name;

private Integer age;

private Integer sex;

// ...

}

public interface LeaderDao {

void insert(Leader teacher);

}

@Repository

public class LeaderDaoImpl implements LeaderDao {

@Autowired

private JdbcTemplate jdbcTemplate;

@Override

public void insert(Leader teacher) {

String insertSql = "insert into t_leader(code, name, age, sex) values(?, ?, ?, ?)";

jdbcTemplate.update(insertSql, teacher.getCode(), teacher.getName(), teacher.getAge(), teacher.getSex());

}

}

@Service

public class LeaderService {

@Autowired

private LeaderDao leaderDao;

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)

public void addLeader(Leader teacher) {

leaderDao.insert(teacher);

int num = 1 / 0;

}

}

测试程序

public class Main {

ApplicationContext applicationContext = new AnnotationConfigApplicationContext("world.xuewei.transaction.annotation");

@Test

public void test() {

LeaderService leaderService = applicationContext.getBean(LeaderService.class);

Leader leader = new Leader();

leader.setCode("1002");

leader.setName("李四");

leader.setSex(1);

leader.setAge(45);

leaderService.addLeader(leader);

}

}

Spring事务接口

Spring 框架中,事务管理相关最重要的 3 个接口如下:

PlatformTransactionManager:平台事务管理器,Spring 事务策略的核心。TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。TransactionStatus:事务运行状态。

我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinition 和 TransactionStatus 这两个接口可以看作是事务的描述。

PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

PlatformTransactionManager

Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是:PlatformTransactionManager。

通过这个接口,Spring 为各个平台如:JDBC、MyBatis(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

PlatformTransactionManager 接口的具体实现如下:

此接口中定义了三个方法:

public interface PlatformTransactionManager {

// 获得事务

TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

// 提交事务

void commit(TransactionStatus var1) throws TransactionException;

// 回滚事务

void rollback(TransactionStatus var1) throws TransactionException;

}

为什么要定义或者说抽象出来PlatformTransactionManager这个接口呢?

主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。

TransactionDefinition

事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务,这个方法里面的参数是 TransactionDefinition 类 ,这个类就定义了一些基本的事务属性。

事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。

事务属性包含了 5 个方面:

隔离级别传播行为回滚规则是否只读事务超时

TransactionDefinition 接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。

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();

}

TransactionStatus

TransactionStatus 接口用来记录事务的状态,该接口定义了一组方法,用来获取或判断事务的相应状态信息。

PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象。

TransactionStatus 接口内容如下

public interface TransactionStatus{

// 是否是新的事务

boolean isNewTransaction();

// 是否有恢复点

boolean hasSavepoint();

// 设置为只回滚

void setRollbackOnly();

// 是否为只回滚

boolean isRollbackOnly();

// 是否已完成

boolean isCompleted;

}

Spring 的事务属性

Spring 的事务属性是用来设置事务管理器的行为特性,可以通过在方法上添加注解或 XML 配置文件中配置来定义事务属性。

以下是 Spring 事务属性中常用的一些选项:

传播行为(Propagation):指在嵌套事务中如何控制事务的传播,常用值包括:

REQUIRED:使用当前事务,如果不存在则创建一个新的事务。SUPPORTS:支持当前事务,如果不存在则以非事务方式执行。MANDATORY:使用当前事务,如果不存在就抛出异常。REQUIRES_NEW:创建一个新的事务,如果当前存在事务则挂起当前事务。NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务则挂起当前事务。NEVER:以非事务方式执行操作,如果当前存在事务就抛出异常。NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前不存在事务,则创建一个事务并执行。嵌套事务可以独立提交或回滚,也可以随父事务一起提交或回滚。 隔离级别(Isolation):指并发事务之间如何隔离交错,常用值包括:

DEFAULT:使用默认隔离级别。(数据库默认的隔离级别,例如 MySQL 默认为 REPEATABLE READ)READ_UNCOMMITTED:未提交读,最低的隔离级别,允许读取其他事务未提交的数据,容易出现脏读、不可重复读和幻读等问题。READ_COMMITTED:已提交读,保证一个事务提交后另一个事务才能读取修改过的数据,避免脏读问题,但会出现不可重复读和幻读问题。REPEATABLE_READ:可重复读,保证一个事务在整个过程中多次读取数据时,数据的状态不发生变化,避免出现脏读和不可重复读问题,但依然会出现幻读问题。SERIALIZABLE:串行化,最高的隔离级别,完全隔离各个事务,一次只允许一个事务访问一个数据,也是最耗费资源的隔离级别。 回滚规则(Rollback rules):指当哪些异常出现时需要回滚事务,可使用通配符指定回滚规则,如 java.lang.Exception,也可以使用类名称指定具体的异常类,如 com.example.MyException。读写规则(Read-only rules):指是否允许当前事务进行读写操作,默认值为 false,表示该事务既可以进行读操作也可以进行写操作,当设置为 true 时表示只允许进行读操作。超时时间(Timeout):指事务允许执行的最长时间,超时后事务将被回滚。默认值为 -1,表示不设置超时时间。

事务属性详解

在 Spring 中,可以通过两种方式来设置事务属性:注解方式和 XML 配置方式。无论是注解方式还是 XML 配置方式,都需要事先配置好事务管理器(例如 DataSourceTransactionManager)并指定给 的 transaction-manager 属性。

这样,在调用被设置了事务属性的方法时,Spring 会自动开启、提交或回滚事务,以保证数据的一致性和完整性。

注解方式

在类或者方法上添加 @Transactional 注解来设置事务属性。示例代码如下:

@Service

public class UserService {

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)

public void updateUser(User user) {

// 业务逻辑代码

}

}

在上述示例中,使用了 @Transactional 注解,并指定了传播行为为 Propagation.REQUIRED、隔离级别为 Isolation.READ_COMMITTED。

XML 配置方式

在 Spring 的配置文件(如 applicationContext.xml)中使用 标签来配置事务属性。示例配置代码如下:

传播行为(Propagation)

Spring 的事务传播属性主要用于解决多个事务方法之间相互调用时,事务如何进行传播和管理的问题。当一个事务方法调用另一个事务方法时,事务传播属性定义了被调用方法应该如何处理事务。大事务中嵌套了很多小事务,它们彼此影响,最终导致最外层大的事务丧失了事务的原子性。

事务传播属性可以控制事务的边界和范围,以确保数据的一致性和完整性。它解决了以下几个问题:

嵌套事务:当一个事务方法内部调用另一个事务方法时,是否创建一个新的事务或者加入已存在的事务。嵌套事务允许在一个事务中存在多个子事务,每个子事务都有自己的保存点,可以独立地进行提交或回滚。通过事务传播属性,可以控制是否开启新的嵌套事务。事务边界:当一个事务方法被另一个非事务方法调用时,是否开启新的事务。如果事务方法被非事务方法调用,那么根据事务传播属性的设置,可以选择开启新的事务或者不开启事务。多事务方法协作:当多个事务方法相互协作完成一个复杂的业务逻辑时,事务传播属性可以确保这些方法都在同一个事务中执行,以保证数据的一致性。通过将事务传播属性设置为 REQUIRED,可以要求被调用方法必须在一个已存在的事务中执行,如果不存在事务,则会开启新的事务。事务的隔离性:事务传播属性还可以影响事务的隔离级别。当一个事务方法被另一个事务方法调用时,事务传播属性可以决定被调用方法使用的事务隔离级别。例如,如果将事务传播属性设置为 REQUIRES_NEW,被调用方法将在一个新的事务中执行,使用独立的隔离级别。

Spring 的事务传播属性可以配置以下七个值,每个值都代表不同的含义和行为:

REQUIRED:如果当前已经存在一个事务,则加入该事务,否则新建一个事务。这是默认值。主要用于增删改的方法。SUPPORTS:如果当前已经存在一个事务,则加入该事务,否则以非事务的方式执行。主要应用在查询方法。REQUIRES_NEW:每次都会新建一个事务,如果当前已经存在一个事务,则将当前事务挂起(暂停),始终采用独立事务方法,主要用于日志记录的方法。MANDATORY:如果当前已经存在一个事务,则加入该事务,否则抛出异常,不常用。NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在一个事务,则将当前事务挂起(暂停),不常用。NEVER:以非事务的方式执行操作,如果当前存在一个事务,则抛出异常,不常用。NESTED:如果当前已经存在一个事务,则在该事务内嵌套一个子事务,否则新建一个事务。如果子事务失败,则只回滚子事务,而不回滚父事务。如果父事务失败,则回滚所有事务。

在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

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;

}

}

REQUIRED

使用的最多的一个事务传播行为,我们平时经常使用的 @Transactional 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:

如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有 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

}

}

REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

举个例子:如果我们上面的 bMethod() 使用 PROPAGATION_REQUIRES_NEW 事务传播行为修饰,aMethod 还是用 PROPAGATION_REQUIRED 修饰的话。如果 aMethod() 发生异常回滚,bMethod() 不会跟着回滚,因为 bMethod() 开启了独立的事务。但是,如果 bMethod() 抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,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

}

}

NESTED

如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与 REQUIRED 类似的操作。也就是说:

在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。如果外部方法无事务,则单独开启一个事务,与 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

}

}

MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

这个使用的很少,就不举例子来说了。

若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚,这里不对照案例讲解了,使用的很少。

TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

更多关于事务传播行为的内容请看这篇文章:《太难了~面试官让我结合案例讲讲自己对 Spring 事务传播行为的理解。》

隔离级别(Isolation)

Spring 的事务隔离属性主要是用来解决并发事务可能导致的数据不一致问题(多个事务在相差极小的同一时间操作相同的数据)。在数据库中,多个事务同时进行时,可能会出现脏读(Dirty Read)、不可重复读(Non-Repeatable Read)、幻读(Phantom Read)和丢失更新(Lost Update)等问题。事务隔离级别就是用来解决这些问题的。

Spring 事务管理的隔离级别有以下几种:

DEFAULT:这是默认值,使用后端数据库默认的隔离级别。MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别。READ_UNCOMMITTED:最低的隔离级别,使用这个隔离级别很少。这种隔离级别会导致很多并发问题,如脏读、不可重复读和幻读。READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。SERIALIZABLE:这是最高的隔离级别,所有的事务依次逐个执行,即事务序列化,完全避免了并发问题。但是这也是代价最大的事务隔离级别,因为它会导致数据库性能明显下降。

需要注意的是,较高的隔离级别可能会对并发性能产生一定的影响,因此在选择隔离级别时需要综合考虑应用程序的并发访问情况和性能需求。

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;

}

}

脏读

一个事务读取了另一个事务未提交的数据。

时间事务 A(存款)事务 B(取款)T1开始事务-T2-开始事务T3-查询余额(当前余额 1000 元)T4-取出余额(当前余额 0 元)T5查询余额(当前余额 0 元)-T6-撤销事务(当前余额 1000 元)T7存入500元(当前余额 500元)-T8提交事务-

图中表格可以分析出,事务 A 读取到了 事务 B 未提交的事务,以为余额为 0 元,但是此时事务 B 撤销事务后,事务 A 将当前余额(读取的脏数据 0 元)更新为 500 元。

Spring 解决方案:

@Transactional(isolation = Isolation.READ_COMMITTED)

不可重复读

在同一事务内,多次读取同一数据返回的结果有所不同。

时间事务 A(存款)事务 B(取款)T1开始事务-T2查询余额(当前余额 1000 元)开始事务T3-查询余额(当前余额 1000 元)T4-取出余额(当前余额 0 元)T5-提交事务(当前余额 0 元)T6再次查询(当前余额 0 元)-

图中表格可以分析出,事务 A 第一次查询的时候余额为 1000,此时事务 B 操作了数据,将余额更新为 0 并提交,事务 A 再次查询的时候余额变成了 0。因为事务 B 更新数据后已经提交,所以事务 A 再次查询到的数据不是脏数据。

Spring 解决方案(本质上是在数据库此条记录加入了行级锁):

@Transactional(isolation = Isolation.REPEATABLE_READ)

幻读

在一个事务内读取了几行记录后,另一个并发事务插入了一些记录,之后,第一个事务再次读取记录,发现多了几行。

时间事务 A(统计金额)事务 B(新建账户)T1开始事务-T2统计总存款金额为 10000 元开始事务T3--T4-新增银行账户,存入 2000 元T5-提交事务T6再次统计总存款金额为 12000 元-

图中表格可以分析出,事务 A 再次统计时,读取到了 事务 B 提交的新增数据。造成了两次统计结果不一致的情况。

不可重复读和幻读的区别是:前者是指读到了已经提交的事务的更改数据(修改或删除),后者是指读到了其他已经提交事务的新增数据。

不可重复读需要添加行级锁,幻读需要加表级锁。

Spring 解决方案(本质上是在数据库此条记录加入了表级锁):

@Transactional(isolation = Isolation.SERIALIZABLE)

丢失更新

两个并发的事务都读取了同一个数据库记录,然后基于这个记录的当前值,都做了修改,然后第一个事务把它的修改写进数据库,然后第二个事务也把它的修改写进数据库,覆盖了第一个事务的修改结果。

Spring 解决方案(本质上是在数据库此条记录加入了行级锁):

@Transactional(isolation = Isolation.REPEATABLE_READ)

数据库对于隔离属性的支持

数据库READ_COMMITTEDREPEATABLE_READSERIALIZABLEMySQL支持支持支持Oracle支持不支持支持

Oracle 不支持 REPEATABLE_READ 属性,它是通过多版本比对的方式解决不可重复读的问题。

默认隔离属性

如果我们没有在 @Transactional 注解中指定 isolation,那么 Spring 会默认指定为 ISOLATION_DEFAULT,表示按照对应的数据库的默认隔离属性。

MySQL 的默认隔离属性为 REPEATABLE_READ,可以通过以下 SQL 查看:

select @@tx_isolation;

Oracle 的默认隔离属性为 READ_COMMITTED,可以通过以下 SQL 查看:

SELECT s.sid, s.serial#,

CASE BITAND(t.flag, POWER(2, 28))

WHEN 0 THEN 'READ COMMITTED'

ELSE 'SERIALIZABLE'

END AS isolation_level

FROM v$transaction t

JOIN v$session s ON t.addr = s.taddr

AND s.sid = sys_context('USERENV', 'SID');

隔离属性在实战中的建议

推荐使用 Spring 默认指定的 ISOLATION_DEFAULT,会根据不同的数据库,选择不同的隔离级别,而且并发的情况其实非常少,真遇到并发可以通用 MyBatis 自定义拦截器开发乐观锁的方式来解决(进行版本比对)。

回滚规则(Rollback rules)

Spring 的事务异常属性主要解决事务回滚和异常处理的问题。

在 Spring 中,事务管理器会捕获事务方法中抛出的异常,并根据异常类型和事务配置进行相应的处理。通过设置事务异常属性,可以指定哪些异常需要回滚事务,哪些异常需要忽略或转换为其他异常类型。

具体来说,Spring 的事务异常属性包括以下几种:

rollbackFor:指定哪些异常需要回滚事务。当事务方法抛出指定类型的异常时,事务管理器将回滚当前事务。例如,@Transactional(rollbackFor = RuntimeException.class) 表示当事务方法抛出 RuntimeException 及其子类异常时,事务将回滚,默认值。noRollbackFor:指定哪些异常不需要回滚事务。当事务方法抛出指定类型的异常时,事务管理器将不会回滚当前事务。例如,@Transactional(noRollbackFor = IOException.class) 表示当事务方法抛出 IOException 及其子类异常时,事务不会回滚。rollbackForClassName:与 rollbackFor 类似,但是指定异常类型的全限定类名。例如,@Transactional(rollbackForClassName = "java.lang.RuntimeException") 表示当事务方法抛出 RuntimeException 及其子类异常时,事务将回滚。noRollbackForClassName:与 noRollbackFor 类似,但是指定异常类型的全限定类名。例如,@Transactional(noRollbackForClassName = "java.io.IOException") 表示当事务方法抛出 IOException 及其子类异常时,事务不会回滚。

通过设置事务异常属性,可以灵活地控制事务的回滚和异常处理。这可以避免因为一些不必要的异常导致事务回滚,从而提高系统的可靠性和性能。同时,也可以将一些不可避免的异常转换为其他异常类型,以便更好地进行异常处理和日志记录。

Spring 默认对捕获 RuntimeException 类以及子类执行回滚操作,对 Exception 以及子类执行提交操作。

建议:实战中使用 RuntimeExceptin 及其子类,使用事务异常属性的默认值

@Transactional(rollbackFor = {Exception.class, RuntimeException.class})

读写规则(Read-only rules)

Spring 的事务的只读属性主要解决对于只读操作的优化问题。

当一个事务方法中只包含读取数据库的操作,而没有任何修改数据的操作时,可以将该事务标记为只读事务。通过设置只读属性,可以告诉数据库引擎在执行这个事务期间采取一些优化措施,以提高性能和并发度。

只读事务的主要优势在于:

提高性能:只读事务不需要对数据进行修改和锁定,因此可以减少数据库引擎的工作量和资源消耗。数据库引擎可以针对只读事务进行一些优化,例如跳过日志记录、减少锁的竞争等,从而提高查询性能。增加并发度:只读事务不会对数据进行修改,因此多个只读事务可以并发地执行,而不会相互干扰。这可以提高系统的并发度和吞吐量,减少用户的等待时间。减少风险:只读事务不会修改数据,因此不会引入数据一致性的风险。如果一个方法只需要读取数据而不需要修改数据,将其标记为只读事务可以确保数据的完整性和一致性。

需要注意的是,只有在确定方法确实只涉及读取操作时,才应该将事务标记为只读。如果方法中包含了任何修改数据的操作,将其标记为只读事务可能导致数据不一致的问题。

@Transactional 注解中,只读属性默认为 false,要开启需要手动配置。

@Transactional(readOnly = true)

在开发中我们需要在只发出查询语句的方法上添加 @Transactional(readOnly = true) 注解,将之申明为只读事务。

多条查询下要使用该注解,能够防止多次查询到的数据不一致(维持可重复读),而且有一定的优化尽管在单条查询下不会出现数据不一致现象,但是使用 @Transactional(readOnly = true) 注解能够优化查询,源码中提到 readOnly = true 也存在着可能的优化

加上 @Transactional(readOnly = true) 可以保证读一致性和查询优化以及一些可能的优化,即使数据库和驱动底层不支持 readOnly 属性,那也不会报错。我们何乐而不为呢?

超时时间(Timeout)

Spring 的事务超时属性主要解决事务执行时间过长导致资源浪费和阻塞问题。

当一个事务方法执行时间过长,可能会占用数据库连接和其他资源,导致其他事务无法及时获取到资源,从而产生阻塞和性能问题。为了避免这种情况的发生,可以设置事务超时属性,告诉 Spring 事务管理器在一定时间内强制结束当前事务。

具体来说,Spring 的事务超时属性可以指定一个时间限制(以秒为单位,超出指定时间将抛出异常:TransactionTimedOutException),如果事务方法执行时间超过这个限制,则事务管理器将自动回滚该事务,并释放相关资源。这可以避免事务执行时间过长导致资源浪费和阻塞的问题,提高系统的可靠性和性能。

需要注意的是,事务超时属性只对长时间运行的事务有效。对于短时间运行的事务,设置超时属性可能会增加系统的开销和复杂性。因此,在使用事务超时属性时,应该根据具体业务需求和系统性能进行权衡和调整。

// 当前事务最多等待 5 秒

@Transactional(timeout = 5)

超时属性的默认值为:-1,表示由对应的数据库来决定。

事务属性常用配置总结

隔离属性:默认即可传播属性:对于增删改方法使用 Required(默认值),对于查询方法使用 Supports回滚规则:默认即可读写规则:对于增删改方法使用 false(默认值),对于查询方法使用 true超时时间:默认即可

增删改的配置如下:

@Transactional

查询的配置如下:

@Transactional(propagation=Propagation.SUPPORTS, readOnly=true)

@Transactional注解详解

作用范围

方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。接口:不推荐在接口上使用。

常用配置参数

注解源码如下,里面包含了基本事务属性的配置:

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Inherited

@Documented

public @interface Transactional {

@AliasFor("transactionManager")

String value() default "";

@AliasFor("value")

String transactionManager() default "";

Propagation propagation() default Propagation.REQUIRED;

Isolation isolation() default Isolation.DEFAULT;

int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

boolean readOnly() default false;

Class[] rollbackFor() default {};

String[] rollbackForClassName() default {};

Class[] noRollbackFor() default {};

String[] noRollbackForClassName() default {};

}

@Transactional 的常用配置参数总结(只列出了 5 个我平时比较常用的):

属性名说明propagation事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过isolation事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过timeout事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。readOnly指定事务是否为只读事务,默认值为 false。rollbackFor用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。

注解原理

面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧!

我们知道,@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。

多提一嘴:createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {

if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {

Class targetClass = config.getTargetClass();

if (targetClass == null) {

throw new AopConfigException("TargetSource cannot determine target class: " +

"Either an interface or a target is required for proxy creation.");

}

if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {

return new JdkDynamicAopProxy(config);

}

return new ObjenesisCglibAopProxy(config);

}

else {

return new JdkDynamicAopProxy(config);

}

}

.......

}

如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是 TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

TransactionInterceptor 类中的 invoke()方法内部实际调用的是 TransactionAspectSupport 类的 invokeWithinTransaction()方法。由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。

Spring AOP 自调用问题

当一个方法被标记了 @Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。

这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。

MyService 类中的method1()调用method2()就会导致method2()的事务失效。

@Service

public class MyService {

private void method1() {

method2();

//......

}

@Transactional

public void method2() {

//......

}

}

解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。

@Service

public class MyService {

private void method1() {

((MyService)AopContext.currentProxy()).method2(); // 先获取该类的代理对象,然后通过代理对象调用method2。

//......

}

@Transactional

public void method2() {

//......

}

}

上面的代码确实可以在自调用的时候开启事务,但是这是因为使用了 AopContext.currentProxy() 方法来获取当前类的代理对象,然后通过代理对象调用 method2()。这样就相当于从外部调用了 method2(),所以事务注解才会生效。我们一般也不会在代码中这么写,所以可以忽略这个特殊的例子。

注意事项总结

@Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败;被 @Transactional 注解的方法所在的类必须被 Spring 管理,否则不生效;底层使用的数据库必须支持事务机制,否则不生效;

相关推荐