Spring事务的简单介绍

687 阅读27分钟

Spring事务的基本原理

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

  1. 获取连接 Connection con = DriverManager.getConnection()

  2. 开启事务con.setAutoCommit(true/false);

  3. 执行CRUD

  4. 提交事务/回滚事务 con.commit() / con.rollback();

  5. 关闭连接 conn.close();

使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子

  1. 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。

  2. spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。

  3. 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

Spring的事务机制

所有的数据访问技术都有事务处理机制,这些技术提供了API用来开启事务、提交事务来完成数据操作,或者在发生错误的时候回滚数据。而Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个PlatformTransactionManager接口,不同的数据访问技术的事务使用不同的接口实现,如表所示:

数据访问技术

实现

JDBC

DataSourceTransactionManager

JPA

JapTransactionManager

Hibernate

HibernateTransactionManager

JDO

JdoTransactionManager

分布式事务

JtaTransactionManager

在程序中定义事务管理器的代码如下:

@Bean 
public PlatformTransactionManager transactionManager() { 
	JpaTransactionManager transactionManager = new JpaTransactionManager(); 
	transactionManager.setDataSource(dataSource()); 
	return transactionManager; 
}

声名式事务

Spring支持声名式事务,即使用注解来选择需要使用事务的方法,它使用@Transactional注解在方法上表明该方法需要事务支持。这是一个基于AOP的实现操作。

import org.springframework.transaction.annotation.Transactional;
@Transactional 
public void saveSomething(Long  id, String name) { 
    //数据库操作 
}

@Transactional 注解属性

  • rollbackFor:触发回滚的异常,默认是RuntimeExceptionError

  • isolation: 事务的隔离级别,默认是Isolation.DEFAULT也就是数据库自身的默认隔离级别,如MySQL是ISOLATION_REPEATABLE_READ可重复读

ps:网络上还说要在@SpringBootApplication上添加注解@EnableTransactionManagement,已经不需要了

Spring 事务的传播属性

Spring 事务传播性是指当多个含有事务的方法嵌套调用时,这多个方法处理事务的规则。比如下图,当事务方法A调用事务方法B时,内层事务B会合并到外层调用A方法的事务中,还是会开启自己的事务。另外如果合并到外层事务,那么当内层方法回滚时后,外层方法会不会回滚。在日常开发环境中,我们最常用到的事务传播行为只有两种,一是 REQUIRED,二是 REQUIRES_NEW

propagation_required

spring默认的事务传播行为是propagation_required,它指的是如果外层调用方法A已经开启事务,那么当前方法B就会加入到外层方法、如果外层方法没有开启事务,那么当前事务就会开启一个事务。这种传播行为可以保证多个嵌套的事务方法在同一个事务内执行,也就是说可以保证多个事务方法同时提交或同时回滚。这个机制满足大多数业务场景。

propagation_requires_new

这个传播行为是每次都新开启一个事务。如果外层调用方法A已经开启了事务,就先把外层的事务挂起,然后执行当前方法B的新事务,执行完毕后再恢复上层事务的执行。这样当内层方法B抛出异常回滚自己的事务时,不会影响外层事务方法的执行

propagation_supported

这个传播行为是指,如果外层调用方法A开启了事务,那当前方法B加入到外层事务。如果外层A不存在事务,那么当前方法B也不会创建新事务,直接使用非事务方式执行。

propagation_not_supported

这个传播行为不支持事务。也就是如果外层调用者开启了事务,就挂起外层事务 ,然后以非事务方式执行当前方法逻辑,等执行完毕后,再恢复外层事务的执行。

propagation_never

这个传播行为不支持事务。也就是说如果外层调用者A开启了事务,就执行当前方法B前会抛出异常。就是说调用方A不能开始事务

propagation_mandatory

这个传播行为是说,配置了这个传播性的方法只能在已经存在事务的方法中被调用。如果在不存在事务的方法中被调用,会抛出异常。就是说调用方A必须开始事务,否则被调用方B会报错

proppagation_nested(嵌套的)

当外层调用方存在事务时,当前方法B合并到外层事务,如果外层不存在事务,就当前开启事务,这点和 PROPAGATION_REQUIRED 传播性一致,不同的是,PROPAGATION_NESTED传播行为的特点是可以保存状态保存点,当事务回滚时,可以回滚到某一个保存点上,从而避免所有嵌套事务都回滚。下面通过一个例子来介绍下它的传播行为:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void methodA() {
    //1.1 做自己的入库操作
    insert()
    System.out.println("save something to db");
    //1.2 调用服务 B 的入库操作,  savepoint  savepoint  savepoint  savepoint  savepoint
    try{
    	serviceB.methodB();
    }catch(Exception e){
  		
    }
    //1.3 执行更新操作
    update();
}
@Service
public class ServiceB {
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public void methodB() {
      insert();
      System.out.println("save something to db");
}

在这段代码中,methodA 执行时,如果调用方没开启事务,它就会开启一个事务,然后执行插入操作;执行 1.2 时由于 methodB 传播性为 NESTED,所以会新建一个 savepoint,用于标记代码 1.1 执行的插入操作。然后 methodB 的执行是使用了 methodA 开启事务的。假如methodB 执行抛出异常时,methodB 的插入操作会被回滚掉,但是 methodA 中的代码 1.1 的 insert 操作不会回滚,这是因为执行 methodB 前建立了 savepoint 点,methodB 的回滚只会回滚到这个 savepoint 点创建的时刻,并且这里当 methodB 回滚后,代码 1.1 和代码 1.3 的执行都会被提交到数据库

不同的业务场景下如何选择合适的隔离界别

// TODO

数据库隔离级别

隔离级别

隔离级别

隔离级别的值

导致的问题

Read-Uncommitted

读未提交

0

导致脏读

Read-Committed

读提交

1

避免脏读,允许不可重复读和幻读

Repeatable-Read

可重复度

2

避免脏读,不可重复读,允许幻读

Serializable

串行化

3

串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重

不考虑隔离性引发安全性问题:

  • 脏读:事务A对数据进行了增删改,但未提交,事务B可以读取到事务A未提交的数据。如果事务A这时候回滚了,那么事务B就读到了脏数据。

  • 不可重复读:事务A中在执行期间发起了两次读操作,第一次读操作和第二次操作之间,事务B对数据进行了update操作,这时候两次读取的数据是不一致的。即一个事务读到了另一个事务已经提交的 update 的数据导致多次查询结果不一致.

  • 幻读:事务A对一定范围的数据进行批量修改,事务B在这个范围insert一条数据,这时候第一个事务就会丢失对新增数据的修改。即一个事务读到了另一个事务已经提交的 insert 的数据导致多次查询结果不一致.

不同的业务场景应该选择哪种隔离界别的呢

  • 银行对账单就不允许幻读的出现

// TODO

Spring中的隔离级别

常量

解释

ISOLATION_DEFAULT

这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。

ISOLATION_READ_UNCOMMITTED

这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。

ISOLATION_READ_COMMITTED

保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。

ISOLATION_REPEATABLE_READ

这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。

ISOLATION_SERIALIZABLE

这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。

// TODO 数据库是通过视图和锁来实现隔离了,Spring的隔离级别还得自己测试下,难不成Spring会在数据库创建视图?

Spring Boot 对事务的支持

通过org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration类。我们可以看出Spring Boot自动开启了对注解事务的支持 Spring

只读事务(@Transactional(readOnly = true))的一些概念

从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。

@Transcational(readOnly=true) 这个注解一般会写在业务类上,或者其方法上,用来对其添加事务控制。当括号中添加readOnly=true, 则会告诉底层数据源,这个是一个只读事务,对于JDBC而言,只读事务会有一定的速度优化。而这样写的话,事务控制的其他配置则采用默认值,事务的隔离级别(isolation) 为DEFAULT,也就是跟随底层数据源的隔离级别,事务的传播行为(propagation)则是REQUIRED,所以还是会有事务存在,一代在代码中抛出RuntimeException,依然会导致事务回滚。

应用场合:

  1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;

  2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。

搬运

深入理解 Spring 事务原理my.oschina.net/xiaolyuh/bl…

极客每日一课,你对Spring传播特性的理解