Spring 之事务管理

文章目录

  • 事务的特性
  • 事务的基本原理
  • 事务并发存在的问题
  • Spring 事务管理 API 分析
    • PlatformTransactionManager
    • TransactionDefinition
    • TransactionStatus
  • 事务的隔离级别
  • 事务的传播行为
  • 事务超时
  • 事务的只读属性
  • 事务的回滚规则
  • Spring 对事务管理的支持
    • 编程式事务管理(基本不用)
    • 声明式事务管理
  • SpringBoot 对事务管理
  • 事务常见问题总结
    • 异常并没有被 ”捕获“ 到
    • 异常被 ”吃“ 掉
    • 事务的范围

事务的特性

事务是指多个操作命令认为是一体的,整体是不可分割的,操作要么全部成功,要么全部失败。四个原则:ACID;原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)持久性(Durability)

事务的基本原理

Mysql 在没有手动开启事务的情况下,默认一条执行命令就是一个事务。

对于 JDBC 操作数据库,想要用到事务,需要按照以下步骤进行:

  • 1、加载数据库驱动:Class.forName("com.mysql.jdbc.Driver");
  • 2、获取连接:Connection con = DriverManager.getConnection();
  • 3、开启事务:con.setAutoCommit(false);
  • 4、创建 statement 实例,专门用来执行 sql Statement statement = con.createStatement();
  • 5、执行CRUD
  • 6、提交事务/回滚事务:con.commit() / con.rollback();
  • 7、关闭连接:conn.close();

使用 Spring 的事务管理功能后,开启事务、提交事务、回滚事务由 Spring 自动完成。

Spring 自动在 CRUD 之前和之后开启事务和关闭事务的原理如下:

  • 1、配置开启事务注解驱动,在被需要事务管理的类或者方法上加上注解 @Transactional
  • 2、Spring 启动的时候会解析生成相关的bean,为 @Transactional 类和方法生成代理类,然后在代理类中自动把相关的事务操作处理掉了。

事务并发存在的问题

  • 脏读: 一个事务(A)读到了另一个事务(B)未提交的数据。关键词:未提交
    如果一个事务(A)读到另一个事务(B)并未提交的数据,恰好事务(B)由于某些原因导致了事务回滚,那么刚刚事务(A)就相当于读到了实际并不存在的数据。很显然,这种情况是存在问题的。
  • 不可重复读(读取了update并已提交的数据):在一个事务中读取一条数据,但是两次读取的数据不一样,读取了已提交的更新数据。关键词:update
    比如在一个事务(A)中,查询了一次账户余额。这时另一个事务(B)在该账户中扣除一笔钱(比如自动还款)并提交了事务,这时事务(A)再次查询账户余额,发现余额变了,这就不可重复读了。很显然,这种情况同样是存在问题。
  • 幻读(读取了insert、delete并已提交的数据):在一个事务中查询两次,但是第二次比第一次多查询出一些数据;两次查询中间有别的事务插入数据了并事务已提交。关键词:insert、delete
  • 丢失更新: 事务提交时,把其他事务已提交更新的数据覆盖了。

注意:不可重复读重点是修改,而幻读重点是新增或删除。

Spring 事务管理 API 分析

三个高级抽象接口:

  • PlatformTransactionManager 平台相关事务管理器 (控制事务核心API )
  • TransactionDefinition 事务定义信息 (在配置文件,配置中如何对事务进行控制 )
  • TransactionStatus 事务状态信息 (在事务运行某个时刻,当前状态信息 )

三个接口关系:
Spring 进行事务管理对象是 PlatformTransactionManager , 根据配置文件中定义事务管理信息 TransactionDefinition 进行事务管理, 通过TransactionStatus 获取某时事务状态信息。

PlatformTransactionManager

接口定义:

public interface PlatformTransactionManager {
     
	/**
	 * 获取某个时刻事务状态
	 */
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
	/**
	 * 提交事务
	 */
	void commit(TransactionStatus status) throws TransactionException;
	/**
	 * 回滚事务	 
	 */
	void rollback(TransactionStatus status) throws TransactionException;
}

Spring 为不同的持久化框架提供了不同PlatformTransactionManager接口实现:

  • DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况。
  • HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
  • JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。

对 Connection 进行事务管理:DataSourceTransactionManager
对 Hibernate 的 session 进行事务管理:HibernateTransactionManager

TransactionDefinition

在配置文件中,对事务进行配置,对应管理信息 。
包含了事务的静态属性,比如:事务传播行为、事务隔离级别 、超时时间、是否只读等等。
Spring 为我们提供了一个默认的实现类:DefaultTransactionDefinition,该类适用于大多数情况。如果该类不能满足需求,可以通过实现 TransactionDefinition 接口来实现自己的事务定义。

TransactionStatus

某个时刻的事务状态信息。
接口定义:

public interface TransactionStatus extends SavepointManager, Flushable {
     
	/**
	 * 是否为新建事务
	 */
	boolean isNewTransaction();
	/**
	 * 判断是否有保存点
	 */
	boolean hasSavepoint();
	/**
	 * 设置标记状态 
	 */
	void setRollbackOnly();
	/**
	 * 判断事务是否标记为回滚 (代码执行rollback 称为标记回滚,事务还需要提交)
	 */
	boolean isRollbackOnly();
	/**
	 * 事务已经被刷出 (flush和commit 不是一个概念)
	 */
	@Override
	void flush();
	/**
	 * 事务是否完成
	 */
	boolean isCompleted();
}

Spring 之事务管理_第1张图片

事务的隔离级别

为什么会有隔离级别?
事务ACID四个特性 ,含有隔离性 ISOLATION ---- 隔离性导致问题(脏读、不可重复读、 虚读) ----- 数据库为了解决隔离性问题,引入隔离级别。

隔离级别 隔离级别值 脏读 不可重复读 幻读
读未提交(Read uncommitted) 0 可能 可能 可能
读已提交(不可重复读)(Read committed) 1 不可能 可能 可能
可重复读(Repeatable read) 2 不可能 不可能 可能
可串行化(Serializable) 3 不可能 不可能 不可能

Read Uncommitted(读未提交):所有读问题都可能发生,一般不会使用这种隔离级别。
Read Committed(读已提交):只能避免脏读的情况发生,Oracle 的默认隔离级别。
Repetable Read(可重复读):能够避免脏读和不可重复读,MySQL 中 InnoDB 引擎默认的隔离级别。
Serilizable:可以解决所有读问题,但由于是串行执行,性能相当一般,所有通常也不会被使用。

MySQL默认采用Repetable Read隔离级别;Oracle默认采用Read Committed隔离级别。

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT:使用底层数据库默认的隔离级别。
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:允许一个事务可以读取另一个事务修改但还没有提交的数据。可能导致脏读、幻读、不可重复读,基本不会使用该隔离级别。
  • TransactionDefinition.ISOLATION_READ_COMMITTED:允许一个事务只能读取另一个事务已经提交的数据。可防止脏读,但幻读、不可重复读可能发生,推荐值。
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。可以防止脏读、不可重复读,但幻读任然可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,防止脏读、不可重复读以及幻读。但是会严重影响程序的性能,基本不会使用该隔离级别。

事务的传播行为

事务传播行为用于解决两个被事务管理的方法互相调用问题。

七种事务传播行为如下:

  • TransactionDefinition.PROPAGATION_REQUIRED:默认值,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:新建事务;如果当前有事务,就挂起当前事务。新建的事务和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚后,不能回滚内层事务执行的结果。
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务超时

指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。

TIME_DEFAULT :默认超时时间 ,默认值 -1 ,使用数据库默认超时时间

事务的只读属性

在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。

事务的回滚规则

事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。
如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。
也可以根据需要人为控制事务在抛出某些未检查异常时任然提交事务,或者在抛出某些已检查异常时回滚事务。

Spring 对事务管理的支持

Spring 提供两种事务管理方式 : 编程式事务管理 、 声明式事务管理

编程式事务管理(基本不用)

在开发代码中,侵入式事务管理 。

基于底层 API 的编程式事务管理:

public class BankServiceImpl implements BankService {
     

    private BankDao bankDao;
    private TransactionDefinition transactionDefinition;
    private PlatformTransactionManager transactionManager;

    public boolean transfer() {
     
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
        boolean result = false;
        try {
     
            result = bankDao.transfer();
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
     
            result = false;
            transactionManager.rollback(transactionStatus);
            System.out.println("Transfer Error!");
        }
    }
}

配置文件:

<bean id="bankService" class="xxx.BankServiceImpl">
    <property name="bankDao" ref="bankDao"/>
    <property name="txManager" ref="transactionManager"/>
    <property name="transactionDefinition">
        <bean class="org.springframework.transaction.support.DefaultTransactionDefinition">
            <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
        bean>
    property>
bean>

在服务类中增加了两个属性:TransactionDefinition 用于定义一个事务;PlatformTransactionManager 用于执行事务管理操作。

PlatformTransactionManager.getTransaction(…) 方法启动一个事务。
创建并启动了事务之后,便可以开始编写业务逻辑代码,然后在适当的地方执行事务的提交或者回滚。

基于 TransactionTemplate 的编程式事务管理:
基于底层API实现事务管理方式很容易理解,但是,事务管理的代码散落在业务逻辑代码中,破坏了原有代码的条理性,并且每一个业务方法都包含了类似的启动事务、提交/回滚事务的样板代码。
Spring 提供了简化的方法,这就是 Spring 在数据访问层非常常见的模板回调模式。

public class BankServiceImpl implements BankService {
     

    private BankDao bankDao;
    private TransactionTemplate transactionTemplate;

    public boolean transfer() {
     
        return (Boolean) transactionTemplate.execute(new TransactionCallback() {
     
            public Object doInTransaction(TransactionStatus status) {
     
                // 被事务管理的代码 
                Object result;
                try {
     
                    result = bankDao.transfer();
                } catch (Exception e) {
     
                    status.setRollbackOnly();
                    result = false;
                    System.out.println("Transfer Error!");
                }
                return result;
            }
        });
    }
}

配置文件:

<bean id="bankService" class="xxx.BankServiceImpl">
    <property name="bankDao" ref="bankDao"/>
    <property name="transactionTemplate" ref="transactionTemplate"/>
bean>

声明式事务管理

基于AOP思想,在创建目标对象时,为目标进行代理,通过环绕通知,动态为目标添加事务管理代码。

基于 命名空间的声明式事务管理:(最推荐)


<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    
    <property name="dataSource" ref="dataSource" />
bean>


<tx:advice id="txAdvice" transaction-manager="transactionManager">
    
    <tx:attributes>
        
        
        <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false" />
    	
        <tx:method name="create*" propagation="REQUIRED" rollback-for="Exception" />  
        <tx:method name="save*" propagation="REQUIRED" rollback-for="Exception" />  
        <tx:method name="update*" propagation="REQUIRED" rollback-for="Exception" />  
        <tx:method name="remove*" propagation="REQUIRED" rollback-for="Exception" />  
        
        <tx:method name="get*" propagation="SUPPORTS" read-only="true" />
        <tx:method name="load*" propagation="SUPPORTS" read-only="true" />
        <tx:method name="find*" propagation="SUPPORTS" read-only="true" />             
        
        <tx:method name="get*" read-only="true"/>
        
        <tx:method name="*"/>
    tx:attributes>
tx:advice>


<aop:config proxy-target-class="true">
    
    <aop:pointcut id="pointcut" expression="execution(* cn.yq.service.*.*(..))" />
    
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
aop:config>

基于 @Transactional 的声明式事务管理:


<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    
    <property name="dataSource" ref="dataSource" />
bean>


<tx:annotation-driven transaction-manager="transactionManager"/>

在需要管理事务方法或者类上面 应用 @Transactional 注解。

@Transactional注解,支持事务属性配置 :

  • isolation 隔离级别
  • propagation 传播行为
  • timeout 超时时间
  • readOnly 是否只读
  • rollbackFor发生异常回滚
  • noRollbackFor 发生异常不回滚

SpringBoot 对事务管理

在SpringBoot中推荐使用@Transactional注解来申明事务。

导入依赖:

<dependency>
    <groupId>org.mybatis.spring.bootgroupId>
    <artifactId>mybatis-spring-boot-starterartifactId>
    <version>1.3.2version>
dependency>

当引入依赖之后,SpringBoot会自动默认注入管理事物的相关对象,所以不需要任何额外配置就可以用@Transactional注解进行事务的使用。

事务常见问题总结

异常并没有被 ”捕获“ 到

案例代码:

@Transactional
public void isertUser2(User user) throws Exception {
      // 插入用户信息 
    userMapper.insertUser(user); // 手动抛出异常 
    throw new SQLException("数据库异常");
}

看上面这个代码,其实并没有什么问题,手动抛出一个 SQLException 来模拟实际中操作数据库
发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,仍然是可以插入一条用户数据的。
那么问题出在哪呢?
因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如抛出的 RuntimeException 就没有问题,但是抛出 SQLException (非运行时异常)就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在@Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class) ,这样就没有问题了,所以在实际项目中,一定要指定异常。

异常被 ”吃“ 掉

案例代码:

@Transactional(rollbackFor = Exception.class)
public void isertUser3(User user) {
     
    try {
     
        // 插入用户信息 
        userMapper.insertUser(user);
        // 手动抛出异常
        throw new SQLException("数据库异常");
    } catch (Exception e) {
     
        // 异常处理逻辑 
    }
}

运行上面的代码,发现,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚。
因为抛出的异常被自己捕获了,并没有往上抛。
那这种怎么解决呢?
直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。

事务的范围

案例代码:

@Override 
@Transactional(rollbackFor = Exception.class) 
public synchronized void isertUser4(User user) {
      
	// 实际中的具体业务…… 
	userMapper.insertUser(user); 
}

因为要考虑并发问题,所以在业务层代码的方法上加了个 synchronized 关键字。

举个实际的场景,比如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一用户信息,不会出现同一数据库中插入了两条相同用户的信息。
但是在压测时,数据库中出现有两条同一用户的信息。

分析原因: 在于事务的范围和锁的范围问题。
从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行
完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。
也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来
了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql
Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状
态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插
入动作,导致了脏数据。
怎么避免这个问题?
第一,把事务去掉即可(不推荐);
第二,在调用该 service 的地方加锁,保证锁的范围比事务的范围大即可。

你可能感兴趣的:(Spring)