深入分析 Spring 事务

 2022-08-29
原文地址:https://cloud.tencent.com/developer/article/1774948

事务介绍(重要)

事务是指一组数据库操作,要么操作都完成,要么操作都不完成(只要有一个未完成,其他操作即使完成也恢复到未完成的状态)。比如A账户向B账户转账,就包括了2个数据库操作:

  1. A账户减少一定额度的资金
  2. B账户增加相同额度的资金

要保证正确转账就必须将转账的2个操作定义到一个事务中,否则就有可能出现A账户转出资金,但是B账户未收到(用户不答应)或 A账户未转资金,而B账户资金增加的情况(银行不答应)。

在实际开发中,会经常涉及事务管理问题,为此 Spring 提供了专门用于事务管理的API。Spring 的事务管理简化了传统事务管理的流程,并且在一定程度上减少了开发者的工作量。Spring 的事务管理分为2种形式:

  • 传统的编程式事务管理:通过编写代码实现的事务管理,包括定义事务的开始、正式执行事务提交和异常时的事务回滚(我们能想到 AOP,这就是把事务代码封装到了 “切面”中,也就是第二种声明式事务管理)
  • 声明式事务管理:通过 AOP 技术实现的事务管理,其主要思想是将事务管理抽取到“切面”,然后通过 AOP 技术将事务管理的“切面”代码织入到业务目标类中。

声明式事务管理使得开发者在配置文件中进行相关的事务规则声明,无须编程,就可以将事务规则应用到业务逻辑中,减少了工作量,提高了开发效率。所以在实际开发中,通常都选用声明式事务管理。

通 AspectJ 实现 AOP 一样,Spring 的声明式事务管理也可以通过2种方式来实现,分别是基于 xml文件注解 的方式。

基于XML方式的声明式事务

通过在配置文件中配置事务规则的相关声明来实现。Spring2.0 以后,提供了 tx 命名空间来配置事务,<tx:advice> 来配置事务的通知/增强处理。使用<aop:advisor><tx:advice> 配置的事务的通知/增强处理与切入点整合起来,让 Spring 自动生成代理。

我们将通过转账来说明如何使用 XML 方式的声明式事务。

1.准备数据,在mysql中新建测试表 account,

202208292239040891.png

准备 2 行默认数据

202208292239053022.png

2.创建 Maven 项目或模块

创建一个名为 springdemo_03 的 Maven 项目或模块。

3.添加依赖

其中包括 mysql 数据库连接包,spring-jdbc 连接数据库工具、junit4 测试等

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

4.编写一个服务类 service.AccountService,在其中定义转账方法 transfer

我们在 AccountService 类中定义:

  • 一个 JdbcTemplate 类型的属性 jdbcTemplate 及其 setter 方法,用于给 spring 注入。JdbcTemplate 是 Spring-jdbc 包中的类,可以简化数据库操作,我们这里就调用其 update 方法修改账户余额。
  • 转账方法 transfer(String outID, String inID, double amt) 第一个参数表示转出资金账户id,第二个参数表示转入资金账户id,第三个参数表示转账金额。
    public class AccountService {
    
        private JdbcTemplate jdbcTemplate;
    
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
            this.jdbcTemplate = jdbcTemplate;
        }
    
        public void transfer(String outID, String inID, double amt) {
            jdbcTemplate.update("update account set balance = balance - ? where id = ?", amt, outID);
            System.out.println("转出资金成功");
            jdbcTemplate.update("update account set balance = balance + ? where id = ?", amt, inID);
            System.out.println("转入资金成功");
            System.out.println("转账成功");
        }
    }

5.编写配置

我们可以在 Spring 配置文件中为 AccountService 对象注入 jdbcTemplate 属性的值,而 jdbcTemplate 对象需要注入 dataSource 属性值才能正确访问数据库,所以 Spring 配置文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation
           ="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
             http://www.springframework.org/schema/aop
             http://www.springframework.org/schema/aop/spring-aop.xsd
             http://www.springframework.org/schema/tx
             http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url"
                      value="jdbc:mysql://localhost/spring_study?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </bean>
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <bean id="accountService" class="service.AccountService">
            <property name="jdbcTemplate" ref="jdbcTemplate"/>
        </bean>
        
    </beans>

6.写测试类

    public class TestTransaction {
        @Test
        public void testTransfer(){
            ApplicationContext ac = new ClassPathXmlApplicationContext("springConfig.xml");
            AccountService accountService = ac.getBean("accountService", AccountService.class);
            accountService.transfer("007","25",10000);
        }
    }

打开数据库,刷新 account 表,可以发现转账成功(一减一增)。

我们在他们的中间制造一个异常,即增加一个除数0异常。

    public void transfer(String outID, String inID, double amt) {
        jdbcTemplate.update("update account set balance = balance - ? where id = ?", amt, outID);
        System.out.println("转出资金成功");
        //制造一个异常
        int i = 1/0;
        jdbcTemplate.update("update account set balance = balance + ? where id = ?", amt, inID);
        System.out.println("转入资金成功");
        System.out.println("转账成功");
    }

在执行测试代码,会出错,打开数据库一看,发现 007 的账户扣款了,24的账户还是不变,说明这个程序已经实现严重的数据不完整性了。

这时候就需要我们的事务来处理了,要么两者都成,要么两者都不成。

7.配置为事务

在 Spring 核心配置文件中进行配置,包括:

  • 增加 aop.tx 约束
  • 配置事务管理器
  • 配置事务通知
  • 配置 aop,在其中将切入点与事务通知整合
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation
                   ="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url"
                      value="jdbc:mysql://localhost/spring_study?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </bean>
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <bean id="accountService" class="service.AccountService">
            <property name="jdbcTemplate" ref="jdbcTemplate"/>
        </bean>
    
        <!-- 配置事务管理器 -->
        <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"/>
            </tx:attributes>
        </tx:advice>
    
        <!-- 配置aop,在其中将切入点与事务通知整合 -->
        <aop:config>
            <aop:pointcut id="ptTx" expression="execution(* service.AccountService.*(..))"/>
            <aop:advisor advice-ref="txAdvice" pointcut-ref="ptTx"/>
        </aop:config>
    
    </beans>

首先恢复 id 为007和24的账户余额都为30000,然后重新测试。

虽然结果输出了转出资金成功,但查看表数据并没有一个更新一个没更新的情况,证明了事务已经开启,保证了数据完整准确。

基于注解方式的声明式事务

基于 XML 方式的声明式事务还是比较麻烦,而基于注解方式的声明式事务则简单很多,开发者只需要关注两件事。

1.在 Spring 核心配置文件中注册事务注解驱动,其代码如下:

        <!-- 配置事务注解驱动 -->
        <tx:annotation-driven transaction-manager="transactionManager"/>

全配置文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation
                   ="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url"
                      value="jdbc:mysql://localhost/spring_study?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </bean>
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <bean id="accountService" class="service.AccountService">
            <property name="jdbcTemplate" ref="jdbcTemplate"/>
        </bean>
    
        <!-- 配置事务管理器 -->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!-- 配置事务注解驱动 -->
        <tx:annotation-driven transaction-manager="transactionManager"/>
    
    <!--    <!– 配置事务通知 –>-->
    <!--    <tx:advice id="txAdvice" transaction-manager="transactionManager">-->
    <!--        <tx:attributes>-->
    <!--            <tx:method name="transfer"/>-->
    <!--        </tx:attributes>-->
    <!--    </tx:advice>-->
    
    <!--    <!– 配置aop,在其中将切入点与事务通知整合 –>-->
    <!--    <aop:config>-->
    <!--        <aop:pointcut id="ptTx" expression="execution(* service.AccountService.*(..))"/>-->
    <!--        <aop:advisor advice-ref="txAdvice" pointcut-ref="ptTx"/>-->
    <!--    </aop:config>-->
    
    </beans>

2.在需要使用事务的bean类或者bean类的方法上添加注解 @Transactional

如果将注解添加到类上,则表示事务的设置对整个类的所有方法都起作用;如果将注解添加在类的某个方法上,则表示事务的设置只对该方法有效。

    package service;
    
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.transaction.annotation.Transactional;
    
    @Transactional
    public class AccountService {
    
        private JdbcTemplate jdbcTemplate;
    
        public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
            this.jdbcTemplate = jdbcTemplate;
        }
        
        public void transfer(String outID, String inID, double amt) {
            jdbcTemplate.update("update account set balance = balance - ? where id = ?", amt, outID);
            System.out.println("转出资金成功");
            //制造一个异常
            int i = 1/0;
            jdbcTemplate.update("update account set balance = balance + ? where id = ?", amt, inID);
            System.out.println("转入资金成功");
            System.out.println("转账成功");
        }
    }

版权属于:乐心湖's Blog 本文链接:https://www.xn2001.com/archives/603.html 声明:博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!