事务介绍(重要)

事务是指一组数据库操作,要么操作都完成,要么操作都不完成(只要有一个未完成,其他操作即使完成也恢复到未完成的状态)。比如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,

准备 2 行默认数据

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&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;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&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;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&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;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"/>

<!--    &lt;!&ndash; 配置事务通知 &ndash;&gt;-->
<!--    <tx:advice id="txAdvice" transaction-manager="transactionManager">-->
<!--        <tx:attributes>-->
<!--            <tx:method name="transfer"/>-->
<!--        </tx:attributes>-->
<!--    </tx:advice>-->

<!--    &lt;!&ndash; 配置aop,在其中将切入点与事务通知整合 &ndash;&gt;-->
<!--    <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("转账成功");
    }
}

关于 @Transactional 的事务提交类型,请查看下文


Last modification:January 24, 2021
如果觉得我的文章对你有用,请随意赞赏