Spring系列(四)——Spring的事务管理

2021-04-17  本文已影响0人  moutory

前言

代码在运行过程中遇到异常,是很常见的事情。但对于一个功能来说,其中可能嵌套了多个持久层的操作,假如代码运行到一半发生错误,我们会希望的结果会是连同之前所有的持久化操作一起进行回滚,保障操作的原子性。本篇文章中,我们将介绍Spring进行事务管理的编程式事务和声明式事务,着重介绍Spring声明式事务管理的XML和注解实现方式,希望对各位读者有所参考。


知识点回顾

在讲事务管理之前,我们先来回顾一下什么是事务,事务逻辑上的一组操作,组成这组操作的各个逻辑单元,要么一起成功,要么一起失败。如果一组逻辑单元没有实现事务的话,那么也就无法保障其内容的一致性。
事务具有原子性、一致性、隔离性和持久性四个特性,我们要想使我们的业务代码满足事务的要求,就需要我们使用好事务管理工具对我们的代码进行管理。

引子

在讲spring事务管理之前,先说一个小例子,假如现在有一个账户表account,我们现在想要进行一个转账操作,具体的实现代码如下(实现的方式是spring+mybatis):

   <!--引入spring测试依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <!--引入Junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--引入mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <!--引入mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
        <!--引入spring集成mybatis依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!--引入C3P0依赖-->
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <!--引入spring-context包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <!--引入spring-jdbc依赖,spring整合mybatis的时候需要使用-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
    <!--引入jdbc文件-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!--配置连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverclass}"></property>
        <property name="jdbcUrl" value="${jdbc.url}"></property>
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>
    <!--配置sqlSessionFactory-->
    <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <!--配置别名-->
        <!--<property name="typeAliasesPackage" value="com.qiqv.code.pojo"></property>-->
    </bean>
    <context:component-scan base-package="com.qiqv"></context:component-scan>
    <!--开启接口扫描-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.qiqv.code.dao"></property>
    </bean>
jdbc.driverclass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.user=root
jdbc.password=root
public interface AccountMapper {
    @Update("update account set total = total + #{count} where uid = #{uid}")
    void plus(@Param("uid") Integer uid, @Param("count") Double count);
    @Update("update account set total = total - #{count} where uid = #{uid}")
    void minus(@Param("uid")Integer userId, @Param("count")Double count);
}
public interface AccountService {
    void transfer(Integer inputAccountId,Integer outputAccountId,Double total);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;
 
    public void transfer(final Integer inputAccountId, final Integer outputAccountId, final Double total) {
                accountMapper.plus(inputAccountId,total);
                int i = 1/0;
                accountMapper.minus(outputAccountId,total);
                System.out.println("交易完成...");
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class SpringTXTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void codeTXTest(){
        accountService.transfer(1,2,500.0);
    }
}
没有事务情况下,出现数据不一致的情况

我们运行测试用例后会发现,由于没有事务的存在,一旦代码运行过程中遇到异常,那么事务的原子性就无法得到保障,出现了一个账户的钱多了,另外一个账户的钱没有对应的减少的情况。基于这个原因,所以在代码层面采取措施来维护数据的原子性是十分有必要的,而Spring也给我们提供了两种方式来进行事务管理。第一种是编程式事务,这种方式需要我们手动地将事务的相关代码加在我们的业务方法上。第二种是声明式事务,我们利用AOP的特性,将事务管理方法和业务方法进行解耦的同时,实现代码的事务管理。

一、Spring的编程式事务

在讲解Spirng的事务管理之前,我们先来了解一下Spring进行事务管理的三个重要组成部分

平台事务管理器 PlatformTransactionManager

PlatformTransactionManager 接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法。

方法 说明
TransactionStatus getTransaction(TransactionDefination defination) 获取事务的状态信息
void commit(TransactionStatus status) 提交事务
void rollback(TransactionStatus status) 回滚事务

需要注意的是,PlatformTransactionManager只是一个接口,我们实际使用的时候需要根据我们的持久层选择来给定不同的实现类。例如:Dao 层技术是jdbcmybatis 时:org.springframework.jdbc.datasource.DataSourceTransactionManager
Dao 层技术是hibernate时:org.springframework.orm.hibernate5.HibernateTransactionManager

事务定义信息 TransactionDefinition

TransactionDefinition 是事务的定义信息对象,里面有如下方法:

方法 说明
int getIsolationLevel() 获得事务的隔离级别
int getPropogationBehavior() 获得事务的传播行为
int getTimeout() 获得超时时间

如果说平台事务管理器决定了实现事务管理的具体技术方案,那么事务定义信息就决定了平台事务管理器在什么约束条件下实现事务的管理。简单举个例子,比如说我们现在想要实现从北京飞往上海的目的,那么我们就可以通过定不同航空公司的机票来完成这个目标,具体选择哪家航空公司实现我们的目的,其实就相当于我们选择不同的持久层框架提供的平台事务管理器。而事务定义信息则可以看成是定义了在什么天气环境下、什么时间限制下的具体约束。
事务定义信息中提到了两个概念,一个是事务的隔离级别,另一个是事务的传播行为
事务的隔离级别很好理解,不同的隔离级别可以解决事务并发产生的问题,如脏读、不可重复读等。
Spring提供了五种事务隔离级别供我们选择。

事务隔离级别
ISOLATION_DEFAULT             和数据库的隔离级别保持一致
ISOLATION_READ_UNCOMMITTED    读未提交
ISOLATION_READ_COMMITTED      读已提交
ISOLATION_REPEATABLE_READ     可重复读
ISOLATION_SERIALIZABLE        串行化
事务传播行为
传播行为 含义
REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常
REQUERS_NEW 新建事务,如果当前在事务中,把当前事务挂起。
NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
NEVER 以非事务方式运行,如果当前存在事务,抛出异常
NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作

一般来说,我们常用的传播行为主要是REQUIREDSUPPORTS

事务状态 TransactionStatus

TransactionStatus 接口提供的是事务具体的运行状态,方法介绍如下。

方法 说明
boolean hasSavepoint() 是否存储回滚点
boolean isCompleted() 事务是否完成
boolean isNewTransaction() 是否是新事务

将这三个事务相关的对象介绍完成后,接下来我们就来使用Spring的编程式事务来完成我们的操作。下面的例子将在Spring整合Mybatis的基础上进行演示,使用mybatis或者其他持久层框架来做dao层的交互其实都没什么所谓,重要的是要理解spring是怎么做事务控制的。

执行后我们可以发现,在没有事务的情况下

步骤一:引入spring-tx模块的依赖

该模块主要用于对外提供事务支持

<!--事务相关-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>
步骤二:在spring配置文件中配置事务管理器等信息
    <!--事务相关配置-->
    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据连接池-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置事务管理模板-->
    <!--直接使用事务管理器操作比较繁琐,所以spring将相关的api进行封装,帮助我们简化使用事务管理器的步骤-->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"></property>
    </bean>
步骤三:在代码中使用spring事务管理模板进行操作

使用注解注入我们的事务管理模板之后,使用事务管理模板对象的execute方法将我们的业务代码进行包裹

@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void transfer(final Integer inputAccountId, final Integer outputAccountId, final Double total) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                accountMapper.plus(inputAccountId,total);
                int i = 1/0;
                accountMapper.minus(outputAccountId,total);
                System.out.println("交易完成...");
            }
        });

    }
}

再次执行我们的测试用例后,我们可以发现,这次测试出现异常后,数据库的数据还是一致的。


使用编程式事务管理测试结果

二、声明式事务管理

Spring 的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明,用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。
声明式事务和编程式事务的依赖和事务管理器等配置,其实都是一样的,区别点只在于编程式事务是把事务管理的代码和业务代码耦合在了一起(或者理解为需要手动写代码),而声明式事务核心在于利用AOP的思想,通过配置的方式,就实现业务代码的事务管理。(也即是无需写代码,只需声明方法是否需要事务管理就行)

(一)XML配置方式实现声明式事务
步骤一:引入spring-tx模块和aspectj的依赖(比编程式事务多了aspectj的依赖 )
        <!--事务相关-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <!--加入aspectj的依赖-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
步骤二:引入tx命名空间
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/tx  http://www.springframework.org/schema/tx/spring-tx.xsd"
步骤三:配置spring核心配置文件

这一步的话主要是配置事务管理器(和编程式事务一样)以及配置aop的事务增强

    <!--事务相关配置-->
    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据连接池-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置事务增强-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--这里*的意思是对于任意一个方法名都加入事务管理中-->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- 将事务增强配置AOP中 -->
    <aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* com.qiqv.code.service.impl.AccountServiceImpl.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"></aop:advisor>
    </aop:config>
步骤四:原有代码不变,进行测试
    @Test
    public void txTest(){
        accountService.transfer(1,2,500.0);
    }
xml配置实现声明式事务测试结果
切点方法的事务参数的配置

在上面的案例中,我们使用了<tx:method>标签来配置我们的切点方法的事务参数
实际上,我们还可以在这个标签中配置更多信息,例如:

<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="-1" read-only="false"/>
(二)注解方式实现声明式事务

使用注解方式实现声明式事务的步骤十分简单,我们只需要在原有代码的基础上做三步即可

步骤一:引入依赖(和xml方式引入的依赖一样)
<!--事务相关-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>
<!--加入aspectj的依赖-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>
步骤二:在需要事务管理的方法上加上@Transactional注解
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
    public void transfer(final Integer inputAccountId, final Integer outputAccountId, final Double total) {
        accountMapper.plus(inputAccountId, total);
        int i = 1 / 0;
        accountMapper.minus(outputAccountId, total);
        System.out.println("交易完成...");
    }
}

我们也可以选择在类上面加上@Transactional注解,这样类中的所有方法就都会根据自己配置的事务信息进行事务管理。

步骤三:在配置文件中配置事务管理器和开启事务的注解驱动
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据连接池-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven></tx:annotation-driven>

这里需要注意,使用注解方式的话,虽然我们可以不用手动在spring配置文件中显式地使用aop进行增强,但是事务管理器的配置还是需要的。同时,对比xml方式的话,学习成本和使用其实是更加简单的,但如果是大批量的进行事务配置修改的话,还是使用xml比较方便,毕竟我们只需要修改配置文件的表达式即可。

至此,Spring的事务管理就介绍到这里。

本篇文章的代码可以从我的gitee上获取,地址如下:https://gitee.com/moutory/spring-tx

结语

回顾本篇文章的内容,主要是介绍了在代码中引入事务管理的重要性——保障我们代码在执行过程中出现异常后的数据一致性。Spring为我们提供了两种方式进行解决,第一种是编程式事务,Spring提供了事务管理模板给我们去使用,这种方式需要我们手动在代码中写入事务代码,耦合性较高,一般不采用这种方式;第二种方式是声明式事务,即使用AOP的思想,显式地声明某些代码需要纳入到事务管理中。声明的方式有两种,分别是XML和注解,使用起来也比较简单。
在实际应用中,我们更多时候是需要关注要对什么样的业务逻辑应用什么样的事务管理条件,这需要我们在工作中不断接触后才能逐渐提升的经验。

上一篇下一篇

猜你喜欢

热点阅读