Spring | 事务管理

Spring | 事务管理

回顾事务

什么是事务

事务是一种数据库机制,如果一个包含多个步骤的业务操作,被事务管理,那么这些操作要么同时成功,要么同时失败,以保证业务的完整操作

事务的四大特性

事务满足 ACID 四大特性,即

  • 原子性 Atomicity:事务是不可分割的最小操作单位,被事务管理的操作要么同时成功,要么同时失败回滚
  • 一致性 Consistency:数据库在事务执行前后都保持一致性状态,在一致性状态下,所有事务对同一个数据的读取结果都是相同的
  • 隔离性 Isolation:多个事务之间是相互独立,也就是说,一个事务所做的修改在最终提交以前,对其它事务是不可见的
  • 持久性 Durability:当事务提交或回滚后,数据库会持久化的保存数据,即使系统发生错误

在使用 Spring 之前如何控制事务

  1. JDBC
// 获取连接
Connection connection = JdbcUtils.getConnection();
// 开启事务:禁止自动提交
connection.setAutoCommit(false);
/**
*   增删改操作
*/
// 提交事务
connection.commit();
// or 回滚事务
connection.rollback();
  1. MyBatis
// 提交事务
sqlSession.commit();
// or 回滚事务
sqlSession.rollback();

实际上,MyBatis 的 SqlSession 的底层依然是 Connection 对象,所以对于事务的控制,本质上就是对 Connection 的控制

Spring 控制事务

Spring 中控制事务有两种实现方式,即声明式事务和编程式事务,首先引入相关依赖

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-tx</artifactId>
	<version>5.2.6.RELEASE</version>
</dependency>

声明式事务

声明式事务管理建立在 Spring 的 AOP 之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚

将 Service 层的类以及数据源事务管理器 DataSourceTransactionManager 纳入 IOC 容器管理,并且开启事务的注解驱动

<bean id="userService" class="cool.yzt.service.UserServiceImpl">
	<property name="userDAO" ref="userDAO"/>
</bean>

<!--DataSourceTransactionManager-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--注入数据源-->
	<property name="dataSource" ref="dataSource"/>
</bean>

<!--开启注解驱动事务,指定事务管理器,proxy-target-class可以指定底层动态代理的实现,true:cglib,默认false:JDK-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/>

声明式事务只需在需要开启事务的方法(通常是进行多个增删改的方法)上添加 @Transactional 注解即可

public class UserServiceImpl implements UserService {
    private UserDAO userDAO;

    // 设置事务的属性,主要是隔离属性和传播属性
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    private transactionTest() {
        /**
        * 增删改
        */
        userDAO.insert();
        userDAO.update();
        userDAO.delete();
    }
}

声明式事务属于无侵入式,使用起来非常简单,不会影响业务逻辑的实现,是 Spring 推荐的方式,但是唯一不足就是粒度较粗,是方法级别

编程式事务

编程式事务时一种侵入式的编程方式,可以对更细粒度的代码块进行事务管理,Spring 中使用 TransactionTemplate 进行编程式事务管理,首先在 IOC 容器中加入 TransactionTemplate 类,将注入所使用的事务管理器

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager">
        <ref bean="transactionManager"/>
    </property>
</bean>

在需要进行事务管理的代码块使用 TransactionTemplate 对象

public class UserServiceImpl implements UserService {
    private UserDAO userDAO;
    private TransactionTemplate transactionTemplate; 
    private transactionTest() {
        // 设置事务的属性,主要是隔离属性和传播属性
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        // TransactionTemplate 的核心方法,需要传入一个TransactionCallback对象,这里直接使用匿名对象实现该接口
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                /**
                * 增删改
                */
                userDAO.insert();
                userDAO.update();
                userDAO.delete();
            }
        });
    }
}

Spring 事务属性

事务的属性即描述一个事务的特征的一系列值,我们可以在 @Transactional 注解中指定一个事务的所有属性

@Transactional(isolation= , propagation= , readOnly= , timeout= , rollbackFor= , noRollbackFor= )

隔离属性

事务应该满足隔离性要求,不同事务之间应该相互独立,但是在并发环境下,隔离性难以保证,设置隔离级别可以解决可能存在的并发一致性问题,Spring 在管理事务中就是通过事务的隔离属性来设置不同事务的隔离级别

常见的并发一致性问题

  1. 脏读
    事务 T2,读取到另一个事务 T1 中修改过但是还未提交(甚至随后会回滚)的数据
  2. 不可重复读
    事务 T2 读取了一个数据,随后事务 T1 对该数据进行了修改,事务 T2 再次读取此数据时(一个很短的时间间隔内),与第一次读取的结果不同,也就是说同一事务中两次读取的结果不同
  3. 幻读
    一个事务 T2 读取某个范围的数据后,另一个事务 T1 向该范围插入新的记录,当事务 T2 再次读取同一范围内的数据时,查询到了原本不存在的记录,也就是说同一事务中对同一表的连续两次的查询到的行数不同

解决方案

  1. 解决脏读
    @Transaction(isolation=Isolation.READ_COMMITTED),即将隔离级别设置为读已提交,一个事务所做的修改在提交之前对其他事务不可见
  2. 解决不可重复读
    @Transaction(isolation=Isolation.REPEATABLE_READ),即将隔离级别设置为不可重复读,通过加行锁实现
  3. 解决幻读
    @Transaction(isolation=Isolation.SERIALIZABLE),即将隔离级别设置为串行化,通过加表锁实现

默认的隔离级别

Spring 会为不同的数据库指定不同的默认的隔离属性(Isolation.ISOLATION_DEFAULT),MySQL 为 REPEATABLE_READ,Oracle 为 READ_COMMITTED,实战中建议隔离属性保持默认即可,这是对性能和并发安全性之间的一个平衡,如果真的遇到并发问题,可以考虑使用乐观锁实现

传播属性

传播属性是对事务嵌套问题的描述,事务嵌套是指一个大的事务中包含多个小的事务,嵌套事务会彼此影响,会导致大事务丧失原子性

传播属性的值外部不存在事务外部存在事务用法备注
REQUIRED开启新的事务融合到外部事务中@Transactional(propagation = Propagation.REQUIRED)增删改方法
SUPPORTS不开启事务融合到外部事务中@Transactional(propagation = Propagation.SUPPORTS)查询方法
REQUIRES_NEW开启新的事务挂起外部事务,创建新的事务@Transactional(propagation = Propagation.REQUIRES_NEW)日志记录方法
NOT_SUPPORTED不开启事务挂起外部事务@Transactional(propagation = Propagation.NOT_SUPPORTED)一般不用
NEVER不开启事务抛出异常@Transactional(propagation = Propagation.NEVER)一般不用
MANDATORY抛出异常融合到外部事物中@Transactional(propagation = Propagation.MANDATORY)一般不用

REQUIRED 是默认的传播属性,所以对于增删改方法的事务,保持默认即可,对于查询方法的事务,显式的指定其传播属性为 SUPPORTS

只读属性

针对只进行查询操作的业务方法,加入只读属性可以提高运行效率,默认为 false

Transactional(readOnly = true)

超时属性

当前事务访问数据时,有可能访问的数据正在被别的事务进行加锁的处理,那么此事务就必须进行等待,超时实行即指定当前事务的最长等待时间,这个属性一般保持默认,由数据库来指定

@Transactional(timeout = 2) // 单位秒,默认值-1

异常属性

异常属性规定了事务中遇到异常的回滚和提交策略,默认对于 RuntimeException 及其子类(运行时异常)采用回滚的策略,对于默认对于 Exception 及其子类(非运行时异常,在编译器就必须处理),采用提交的策略

// 指定会导致事务回滚的异常
@Transactional(rollbackFor = java.lang.Exception.class, xxx, xxx)
// 指定不会导致事务回滚的异常
@Transactional(noRollbackFor = java.lang.Exception.class, xxx, xxx)

属性总结

  1. 隔离属性:保持默认,如果需要更加严格的隔离性控制,可以实现乐观锁
  2. 传播属性:增删改操作保持默认Required,查询操作显示指定为 Supports
  3. 只读属性:增删改操作保持默认,查询操作显示指定为 true
  4. 超时属性:保持默认
  5. 异常属性:保持默认
// 增删改操作
@Transactional
// 查询操作
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)

参考

孙哥说Spring5

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://yzt.cool/archives/spring事务管理