Spring 对于事务上的应用的详细说明

1. 事务概述

什么是事务

在一个业务流程当中,通常需要多条DML(insert delete update) 语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。

多条DML要么同时成功,要么同时失败,这叫做事务。事务(Transaction)

事务的四个处理过程:

  1. 第一步:开启事务(start transaction)
  2. 第二步:执行核心业务代码
  3. 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
  4. 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)

事务的四个特性:

  1. 原子性:事务是最小的工作单元,不可再分
  2. 一致性:事务要求要么同时成功,要么同时失败,事务前和事务后的总量不变
  3. 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰
  4. 持久性:持久性是事务结束的标志。

2. 引入事务场景

以银行账户转账为例学习事务,两个账户 act-001 和 act-002 。act-002 账户转账 10000,必须同时成功,或者同时失败,(一个减成功,一个加成功,这两条update 语句必须同时成功,或同时失败。)连接数据库的技术采用Spring 框架的JdbcTemplate.

首先我在pom.xml 当中先配置对应项目模块需要依赖的 jar包。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spring6-013-tx-bank</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>


    <repositories>
<!--        spring 的版本仓库-->
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


    <dependencies>
        <!--        spring context 依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!--        spring aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!--spring jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--spring aspects依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.13</version>
        </dependency>

        <!--@Resource注解-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- junit4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
    </dependencies>
</project>

2.1 第一步:准备数据库表


2.2 第二步:创建包结构

com.powernode.bank.pojo

com.powernode.bank.service

com.powernode.bank.service.impl

com.powernode.bank.dao

com.powernode.bank.dao.impl

2.3 第三步:准备对应数据库映射的 Bean 类

arduino 复制代码
package com.rainbowsea.bank.pojo;

public class Account {

    private String actno;  // 账户
    private Double balance;  // 金额


    public Account() {
    }

    public Account(String actno, Double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "actno='" + actno + ''' +
                ", balance=" + balance +
                '}';
    }


    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }
}

2.4 第四步:编写持久层

首先定义规范,持久层的规范,通过接口(interface) 来定义约束。

转账:首先我们需要查询对应账户上是否有该满足的余额;如果够,我们就需要更新数据(修改数据);所以定义两个方法就行:根据账户查询,根据账户修改

java 复制代码
package com.rainbowsea.bank.dao;

import com.rainbowsea.bank.pojo.Account;

public interface AccountDao {

    /**
     * 根据账号查询账号信息
     * @param actno
     * @return
     */
    Account selectByActno(String actno);


    /**
     * 更新账号信息
     * @param account
     * @return
     */
    int update(Account account);


}

根据该接口,编写对应持久层的实现类

java 复制代码
package com.rainbowsea.bank.dao.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;


@Repository(value = "accountDaoImpl")  // 交给 spring 管理
public class AccountDaoImpl implements AccountDao {

    
    @Resource(name = "jdbcTemplate")  // jdbcTemplate 内置的对象,resource 根据名称进行 set 注入赋值
    private JdbcTemplate jdbcTemplate;


    @Override
    public Account selectByActno(String actno) {

        String sql = "select actno,balance from t_act where actno = ?";

        // 查询
        Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);

        return account;
    }

    @Override
    public int update(Account account) {
        String sql = "update t_act set balance = ? where actno = ?";
        int count = jdbcTemplate.update(sql, account.getBalance(), account.getActno());
        return count;
    }
}

2.5 第五步:编写业务层

首先定义规范,业务层的规范,通过接口(interface) 来定义约束。

定义一个进行转账操作的业务

java 复制代码
package com.rainbowsea.bank.service;


import com.rainbowsea.bank.pojo.Account;

/**
 * 业务接口
 * 事务就是在这个接口下控制的
 */
public interface AccountService {


    /**
     * 转账业务方法
     * @param fromActno 从这个账户转出
     * @param toActno 转入这个账号
     * @param money 转账金额
     */
    void transfer(String fromActno, String toActno,double money);

}

根据该接口,编写对应业务层的实现类。

java 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import com.rainbowsea.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


@Component(value = "AccountServicelmpl")
public class AccountServicelmpl implements AccountService {


    @Resource(name = "accountDaoImpl")  // @Resource 根据名称进行set 注入赋值
    private AccountDao accountDao;


    // 控制事务: 因为在这个方法中要完成所有的转账业务
    @Override
    public void transfer(String fromActno, String toActno, double money) {

        // 第一步:开启事务

        // 第二步:执行核心业务逻辑

        // 查询转出账号的余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);

        if (fromAct.getBalance() < money) {
            throw new RuntimeException("余额不足,转账失败");
            // 第三步:回滚事务
        }

        // 余额充足
        Account toAct = accountDao.selectByActno(toActno);

        // 将内存中两个对象的余额先修改一下
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);


        // 数据库更新
        int count = accountDao.update(fromAct);

        // 模拟异常
        String s = null;
        s.toString();

        count += accountDao.update(toAct);

        if (count != 2) {
            throw new RuntimeException("转账失败,联系银行");
            // 第三步回滚事务
        }

        // 第三步:如果执行业务流程过程中,没有异常,提交事务
        // 第四五:如果执行业务流程过程中,有异常,回滚事务

    }

   


}

2.6 第六步:编写Spring 配置文件


xml 复制代码
<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


<!--    组件扫描-->
    <context:component-scan base-package="com.rainbowsea.bank"></context:component-scan>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"></property>
        <property name="username" value="root"></property>
        <property name="password" value="MySQL123"></property>
    </bean>


<!--    配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

2.7 第七步:编写表示层(测试程序)

csharp 复制代码
public class SpringTxTest {

    @Test
    public void testNoXml() {
        // Spring6Config.class 对应上的配置类
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
        AccountService accountService = applicationContext.getBean("AccountServicelmpl", AccountService.class);

        try {
            accountService.transfer("act-001","act-002",10000);
            System.out.println("转账成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.8 第八步:模拟异常,测试

通过在 AccountServicelmpl 业务层模拟,null 指针异常,看转账是否成功。

csharp 复制代码
public class SpringTxTest {

    @Test
    public void testNoXml() {
        // Spring6Config.class 对应上的配置类
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
        AccountService accountService = applicationContext.getBean("AccountServicelmpl", AccountService.class);

        try {
            accountService.transfer("act-001","act-002",10000);
            System.out.println("转账成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. 运用 Spring 进行事务处理

3.1 Spring 实现事务的两种方式:

编程式事务:

  • 通过编写代码的方式来实现事务的管理。

声明式事务:

  • 基于注解方式
  • 基于XML配置方式

3.2 Spring 事务管理API

Spring 对事务的管理底层实现方式是基于 AOP实现的,采用 AOP的方式进行了封装,所以Spring 专门针对事务开发了一套API,API的核心接口如下:

PlatformTransactionManager接口:spring 事务管理器的核心接口,在Spring6中它有两个实现:

  • DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
  • JtaTransactionManager:支持分布式事务管理。

如果要在Spring6中使用 JdbcTemplate,就要使用 DataSourceTransactionManager 来管理事务。(Spring 内置写好了,可以直接用)

3.3 声明事务在"注解实现方式"

第一步:spring.xml 配置文件中配置事务管理器。

配置事务管理器,需要根据对应数据源里面的账户密码等信息,管理连接数据库,从而开启事务(开启事务,提交事务,回滚事务)等操作

xml 复制代码
 <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"></property>
        <property name="username" value="root"></property>
        <property name="password" value="MySQL123"></property>
    </bean>


<!--    配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

第二步: 在spring配置文件中引入tx命名空间。

ini 复制代码
<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

</beans>

第三步:spring.xml 配置文件中配置"事务注解驱动器",开始注解的方式控制事务。

是通过上面配置的 事务管理器 进行一个事务注解驱动器的 开启 。因为该事务管理器当中存储着对应数据库的账户和密码等信息(数据源)

xml 复制代码
<!--    配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

<!--    开启事务注解驱动器 : 上面的那个配置事务管理器,进行一个事务注解驱动器-->
    <tx:annotation-driven transaction-manager="txManager"></tx:annotation-driven>

完整的 spring.xml 配置信息如下:

xml 复制代码
<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


<!--    组件扫描-->
    <context:component-scan base-package="com.rainbowsea.bank"></context:component-scan>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"></property>
        <property name="username" value="root"></property>
        <property name="password" value="MySQL123"></property>
    </bean>


<!--    配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>



<!--    配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

<!--    开启事务注解驱动器 : 上面的那个配置事务管理器,进行一个事务注解驱动器-->
    <tx:annotation-driven transaction-manager="txManager"></tx:annotation-driven>
</beans>

第四步: 在service类上或方法上添加@Transactional注解

  • 在类上添加该@Transactional 注解,则表示该类中所有的方法都有事务了(都进行了事务上的控制,回滚了)
  • 在某个方法上添加@Transactional注解,则表示只有这个方法使用了事务(进行了事务上的控制,回滚)其他的方法,并没有进行事务上的控制。


一般加入了事务的同时,也需要交给Spring IOC 容器进行管理

java 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import com.rainbowsea.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


@Component(value = "AccountServicelmpl")
@Transactional
public class AccountServicelmpl implements AccountService {


    @Resource(name = "accountDaoImpl")  // @Resource 根据名称进行set 注入赋值
    private AccountDao accountDao;

    // 控制事务: 因为在这个方法中要完成所有的转账业务
    @Override
    public void transfer(String fromActno, String toActno, double money) {

        // 第一步:开启事务

        // 第二步:执行核心业务逻辑

        // 查询转出账号的余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);

        if (fromAct.getBalance() < money) {
            throw new RuntimeException("余额不足,转账失败");
            // 第三步:回滚事务
        }

        // 余额充足
        Account toAct = accountDao.selectByActno(toActno);

        // 将内存中两个对象的余额先修改一下
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);


        // 数据库更新
        int count = accountDao.update(fromAct);

        // 模拟异常
        String s = null;
        s.toString();

        count += accountDao.update(toAct);

        if (count != 2) {
            throw new RuntimeException("转账失败,联系银行");
            // 第三步回滚事务
        }

        // 第三步:如果执行业务流程过程中,没有异常,提交事务
        // 第四五:如果执行业务流程过程中,有异常,回滚事务

    }

}

运行测试:

虽然出现异常了,再次查看数据库表中数据:通过测试,发现数据没有变化,事务起作用了。

csharp 复制代码
 @Test
    public void testSpringTx() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

        AccountService accountService = applicationContext.getBean("AccountServicelmpl", AccountService.class);

        try {
            accountService.transfer("act-001","act-002",10000);
            System.out.println("转账成功");
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

3.4 事务属性

Spring 当中事务的属性,其实就是: @Transactional 注解当中的属性。

3.4.1 事务包括哪些:

java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

其中多个属性,我们需要更加关注如下几个重点属性:

  1. 事务的传播行为
  2. 事务的隔离级别
  3. 事务超时
  4. 只读事务
  5. 设置出现哪些异常回滚事务
  6. 设置出现哪些异常回滚事务

3.4.2 事务的传播行为

什么是事务的传播行为?

在Service 类中有 A( ) 方法和B( ) 方法,A( ) 方法上有事务,B( ) 方法上也有事务。

当A( ) 方法执行过程中调用了B( ) 方法,事务是如何传递的?

复制代码
是统一合并为一个事务里,
还是开启一个新的事务?

上述操作就是事务传播行为。

事务传播行为在Spring 框架中被定义为枚举类型:

scss 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction.annotation;

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

一共有七种传播行为:

  1. REQUIRED:支持当前事务,如果不存在就新建一个(默认 ) 《没有事务就新建,有就加入事务,简单的说就是共用同一个事务处理》
  2. SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行 《有事务就加入,没有就不管了》
  3. MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常 《有事务就加入事务,没有就抛异常》
  4. REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起。 《不管有没有,直接开启一个新事务,开启的事务和之前的事务不存在嵌套关系,之前的事务被挂起,简单的说,就是不会共用一个事务,而是各自不同的DML生成不同的事务》
  5. NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起事务 《不支持事务,存在就挂起事务》
  6. NEVER:以非事务的方式运行,如果有事务存在,抛出异常 《不支持事务,存在就抛异常》
  7. NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务当中,被嵌套的事务可以独立于外层事务,进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。 《有事务的话,就在这个事务里,再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和独立的回滚。没有事务就和 REQUIRED 一样处理》

为了更好的直观的观察事务的传播行为,这里我们引入:集成Log4j2日志框架,在日志信息中可以看到更加详细的信息。

首先在 pom.xml 配置文件当中引入 Log4j2 日志框架的相关依赖

xml 复制代码
		<dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>

完整的pom.xml 配置文件信息

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spring6-013-tx-bank</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>


    <repositories>
<!--        spring 的版本仓库-->
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


    <dependencies>
        <!--        spring context 依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!--        spring aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!--spring jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--spring aspects依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.13</version>
        </dependency>

        <!--@Resource注解-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- junit4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
    </dependencies>
</project>

在导入配置 Log4j2 的 资源上的配置,xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>

<configuration>

    <loggers>
        <!--
            level指定日志级别,从低到高的优先级:
                ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
        -->
        <root level="DEBUG">
            <appender-ref ref="spring6log"/>
        </root>
    </loggers>

    <appenders>
        <!--输出日志信息到控制台-->
        <console name="spring6log" target="SYSTEM_OUT">
            <!--控制日志输出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
        </console>
    </appenders>

</configuration>

在代码中设置事务的传播行为:

这里,我们测试:REQUIRED:支持当前事务,如果不存在就新建一个(默认 ) 《没有事务就新建,有就加入事务,简单的说就是共用同一个事务处理》

这里我们测试,在 AccountServicelmpl 类当中的 save() 方法创建一个新的账户""act-003", 1000.0",然后在 AccountServicelmpl 类的 save() 方法当中,调用 AccountServicelmpl2类当中的 save( ) 方法,添加 "act-004", 1000.0 新的账户信息。

我们这里添加两个新的账户,一个是"act-003" 是 在AccountServicelmpl 类当中的 save() 方法 保存的,而另一个则是"act-004"账户是在,AccountServicelmpl2 类当中的 save() 方法保存的,同时在这个AccountServicelmpl2 类当中的 save() 方法,添加上异常,导致添加账户失败,按照

我们的REQUIRED:支持当前事务,如果不存在就新建一个(默认 ) 《没有事务就新建,有就加入事务,简单的说就是共用同一个事务处理》 的特点,该两个添加账户的操作,归属于同一个事务,其中一个添加账户信息失败了,就全部失败。事务发生回滚操作。

ini 复制代码
@Transactional(propagation = Propagation.REQUIRED)
kotlin 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import com.rainbowsea.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


@Component(value = "AccountServicelmpl")
public class AccountServicelmpl implements AccountService {


    @Resource(name = "accountDaoImpl")  // @Resource 根据名称进行set 注入赋值
    private AccountDao accountDao;

    @Resource(name = "accountServiceImpl2")
    private AccountService accountService2;

    /**
     * 保护账号信息
     *
     * @param account
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void save(Account account) {

        // 这里调用的dao的 insert ()方法,插入记录
        accountDao.insert(account);  // 保存 act-003 账户信息

        // 创建账号对象
        Account act2 = new Account("act-004", 1000.0);
        // 这里调用 accountServiceImpl2 中的 save() 方法进行插入
        try {
            accountService2.save(act2);
        } catch (Exception e) {

        }

        // 继续往后进行我当前1号事务自己的事儿。

    }


}
kotlin 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import com.rainbowsea.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


@Service(value = "accountServiceImpl2")  // 给Spring 管理起来
public class AccountServiceImpl2 implements AccountService {


    @Resource(name = "accountDaoImpl") // accountDaoImpl 已经交给Spring 管理,所以这里可以直接用 @Resource 根据名称set注入
    private AccountDao accountDao;

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void save(Account account) {
        accountDao.insert(account);

         //模拟异常
        String s = null;
        s.toString();

        // 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
    }
}

运行测试;

这里我们再关闭异常,看看是否添加成功。


下面我们再测试一个:REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起。 《不管有没有,直接开启一个新事务,开启的事务和之前的事务不存在嵌套关系,之前的事务被挂起,简单的说,就是不会共用一个事务,而是各自不同的DML生成不同的事务》 的传播行为。各自用各自的事务。

这里我们把添加的账户信息删除一下,方便后续的操作。

下面我们将 AccountServicelmpl2 类当中的 save() 方法 上的事务传播行为设置为:REQUIRES_NEW 进行测试,再次测试添加两个账户信息的操作。

同样开启对 AccountServicelmpl 2 类当中的 save() 方法,添加上异常。

less 复制代码
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)  // 事务注解:事务的传播行为
    public void save(Account account) {
        accountDao.insert(account);

         //模拟异常
        String s = null;
        s.toString();

        // 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
    }
}

运行测试:

各自使用的是各自的事务进行了控制,不是同一个事务进行控制的

在 AccountServicelmp1 当中的 save() 添加

act-003 账户成功了,并没有受到 AccountServicelmp2

当中的save()的异常的出现的影响,导致添加失败,

因为这两个不同的类当中的 save()方法上,使用的

并不是同一个事务管理的,而是使用的各自不同的事务

管理的,所以AccountServicelmp2 类当中的 save() 发生了异常,导致了 AccountServiceImp2 类

当中的 save() 方法当中的事务,进行了一个事务的回滚,自然就添加失败了。

3.4.3 事务的隔离级别上的设置

事务的隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚,隔音效果越好。数据库中读取数据存在的三大问题:

  • 脏读:读取到没有提交的数据库的数据,叫做脏读
  • 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。(并发,多线程就会涉及的不可重复读)
  • 幻读:读到的数据是假的

事务的隔离级别包括四个级别:

  • 读未提交:READ_UNCOMMITTED

    • 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
  • 读提交:READ_COMMITTED

    • 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题(Oracel 默认)
  • 可重复读:REPEATABLE_READ

    • 解决了不可重复度,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读 问题。MySQL默认 是个隔离级别
  • 序列化:SERIALIZABLE

    • 解决了幻读问题,事务排序执行。但不支持并发。
隔离级别 脏读 不可重复读 幻读
读未提交
读提交
可重复读
序列化

在Spring框架中隔离级别在spring中以枚举类型存在:

java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction.annotation;

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

在Spring 当中事务的隔离级别上的设置,使用注解:

ini 复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)

这里我们测试:事务隔离级别:READ_UNCOMMITTED 和 READ_COMMITTED

怎么测试:一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。

IsolationService2 类 save()方法负责,插入一个账户信息 "act-005",同时睡眠12秒中,当其还在睡眠当中时(没有提交给数据库,而是在内存当中)的时候,我们的IsolationService1 getByActno( ) 方法根据其插入的"act-005" 账户去查,这时候的 act-005 还在内存当中,我们并没有提交给数据库,看看能否查到?

java 复制代码
package com.rainbowsea.bank.service.impl;


import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service(value = "i2")  // 交给Spring 管理
public class IsolationService2 {


    @Resource(name = "accountDaoImpl") // 因为accountDaoImpl已经交给Spring管理了,@Resource复杂类型的set注入赋值
    private AccountDao accountDao;

    // 2号
    //负责insert
    public void save(Account account) throws IOException {
        accountDao.insert(account);
        // 睡眠一会
        try {
            Thread.sleep(1000 * 12);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }


}
kotlin 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


@Service(value = "i1")
public class IsolationService1 {

    @Resource(name = "accountDaoImpl") // 因为 accountDaoImpl 已经交给Spring 管理了,所以可以使用@Resource 进行非简单类型的赋值
    private AccountDao accountDao;


    // 1号
    // 负责查询
    // 当前事务可以读取到别的事务没有提交的数据
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void getByActno(String actno) {
        Account account = accountDao.selectByActno(actno);
        System.out.println("查询到的账户信息: " + actno);
    }
}
java 复制代码
@Test
    public void testIsolation1() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
        i1.getByActno("act-005");
    }

    @Test
    public void testIsolation2(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
        Account act = new Account("act-005", 1000.0);
        try {
            i2.save(act);
        } catch (Exception e) {

        }
    }

运行结果:

下面我们将:其设置为:READ_COMMITTED,就无法脏读了(无法读取到内存当中的信息),只有当对方:对方事务提交之后的数据,我才能读取到。

我们的IsolationService2 也要设置为:READ_COMMITTED,就无法脏读了(无法读取到内存当中的信息),只有当对方:对方事务提交之后的数据,我才能读取到。

java 复制代码
package com.rainbowsea.bank.service.impl;


import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service(value = "i2")  // 交给Spring 管理
public class IsolationService2 {


    @Resource(name = "accountDaoImpl") // 因为accountDaoImpl已经交给Spring管理了,@Resource复杂类型的set注入赋值
    private AccountDao accountDao;

    // 2号
    //负责insert
    // 或者整个异常的子类异常,都不回滚,其他异常回滚
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void save(Account account) throws IOException {
        accountDao.insert(account);
        // 睡眠一会
        try {
            Thread.sleep(1000 * 12);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }。
    }


}
kotlin 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;


@Service(value = "i1")
public class IsolationService1 {

    @Resource(name = "accountDaoImpl") // 因为 accountDaoImpl 已经交给Spring 管理了,所以可以使用@Resource 进行非简单类型的赋值
    private AccountDao accountDao;


    // 1号
    // 负责查询
    // 当前事务可以读取到别的事务没有提交的数据
    //@Transactional(isolation = Isolation.READ_UNCOMMITTED)
    //  对方事务提交之后的数据,我才能读取到
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void getByActno(String actno) {
        Account account = accountDao.selectByActno(actno);
        System.out.println("查询到的账户信息: " + actno);
    }
}

同样我们还是:插入 "act-005" 的账户信息,进行测试,看看还能不能查询到结果了。

通过执行结果可以清晰的看出隔离级别不同,执行效果不同。

3.4.4 事务超时上的设置

在Spring框架的 @Transactional 注解 当中可以设置事务的超时时间:

java 复制代码
@Transactional(timeout = 10)
// 表示设置事务的超时时间为:10秒

表示超过10秒如果该事务中所有的 DML语句还没有执行完毕的话,最终结果会选择回滚。

默认值为 -1;表示没有时间限制。

注意这里有个坑,事务的超时时间指的是哪段时间?
在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很多很多业务逻辑,这些业务代码执行的时间是不被计入超时时间。

如下测试:

我们首先将 DML 语句放在 睡眠 12 秒之前,看看后面的业务处理时间,是否会被记录到超时时间内,会(则超时了,事务会发生回滚);不会(则没有超时,不计入后面的时间,事务不发生回滚)

java 复制代码
package com.rainbowsea.bank.service.impl;


import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service(value = "i2")  // 交给Spring 管理
public class IsolationService2 {

    @Resource(name = "accountDaoImpl") // 因为accountDaoImpl已经交给Spring管理了,@Resource复杂类型的set注入赋值
    private AccountDao accountDao;

      @Transactional(timeout = 10)  // 设置事务超时间为 10
    public void save(Account account) throws IOException {

        accountDao.insert(account);

        // 睡眠一会
        try {
            Thread.sleep(1000 * 12);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }


}

下面我们重新将该添加的"act-003" 的数据删除了。

这次我们将 DML 语句放到 "睡眠 12秒"的最后面,看看事务是否会发生回滚

运行

java 复制代码
@Test
    public void testIsolation2(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
        Account act = new Account("act-005", 1000.0);
        try {
            i2.save(act);
        } catch (Exception e) {

        }
    }

当然,如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句(比如:判断语句之类的)。

3.4.5 只读事务上的设置

如果像让当前事务设置为:只读事务可以用如下代码注解。

ini 复制代码
@Transactional(readOnly = true)

在该事务执行过程中只能读(只允许 select 语句执行"查"),delete , insert, update 均不可执行。

该特性的作用是:启动Spring 框架的优化策略,提高 select 语句执行效率

如果该事务中确实没有增删改操作,建议设置为只读事务,提高查询效率。

3.4.6 设置哪些异常回滚事务

在Spring 框架中可以设置定义哪些异常,进行事务的回滚:

less 复制代码
@Transactional(rollbackFor = XXX异常类.class)
@Transactional(rollbackFor = RuntimeException.class)
表示只有发生RuntimeException异常或该异常的子类异常才回滚。
java 复制代码
@Transactional(rollbackFor = RuntimeException.class)  // 只要发生RuntimeException.class(可以设置其他异常)包含整个异常的子类异常,都回滚,其他异常不回滚
    public void save(Account account) throws IOException {
        accountDao.insert(account);

        if(1 == 1) {
            throw new RuntimeException();
        }
    }
ini 复制代码
  @Test
    public void testIsolation2(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
        Account act = new Account("act-005", 1000.0);
        try {
            i2.save(act);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我们这次将异常换成:throw new IOException(); IO 异常不属于 RuntimeException 异常下的,发生该异常,不会回滚。

3.4.7 设置哪些异常不回滚事务

反过来,同样的在Spring 框架中可以设置定义哪些异常,不进行事务的回滚:

less 复制代码
@Transactional(noRollbackFor = XXX异常类.class)
@Transactional(noRollbackFor = NullPointerException.class)
表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。
java 复制代码
 @Transactional(noRollbackFor = NullPointerException.class)  // NullPointerException(空指针异常).class(可以设置其他异常)或者整个异常的子类异常,都不回滚,其他异常回滚
    public void save(Account account) throws NullPointerException {
        accountDao.insert(account);

        if (1 == 1) {
            throw new NullPointerException();
        }
    }

我们这次将异常换成:throw new RuntimeException();; RUN 异常不属于 NullPointerException异常下的,发生该异常,会进行回滚。

4. 事务的全注解式开发

编写一个类来代替配置文件,代码如下:

注意:对于数据源以及JdbcTemplate, DataSourceTransactionManager 事务上的管理,我们可以使用 @Bean 进行注解式开发:

首先在配置类上,写明如下注解

less 复制代码
@Configuration // 代替sprint.xml 配置文件,在这个类当中完成配置
@ComponentScan("com.rainbowsea.bank")  // 组件扫描
@EnableTransactionManagement // 开启事务

设置数据源信息配置:

Spring 框架,看到这个 @Bean 注解后,会调用这个被标注的方法,这个方法的返回值是一个Java对象,这个Java对象会自动纳入 IOC容器管理,返回的对象就是Spring 容器当中的一个Bean 了。并且这个 Bean 的名字是:dataSource

同 getDataSource 方法()

ini 复制代码
@Bean(name = "xxxx")

使用 public DruidDataSource getDataSource() 方法。

java 复制代码
// 设置数据源信息配置
    /*
    Spring 框架,看到这个@Bean注解后,会调用这个被标注的方法,这个方法的返回值式一个Java对象,
    这个Java对象会自动纳入IOC容器管理,返回的对象就是Spring容器当中的一个Bean了
    并且这个Bean的名字式:dataSource
     */
    @Bean(name = "dataSource")
    public DruidDataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
        dataSource.setUsername("root");
        dataSource.setPassword("MySQL123");

        // 设置好后,返回给Spring 管理
        return dataSource;
    }

配置:JdbcTemplate Spring内置的 JDBC信息。用 public JdbcTemplate getJdbcTemplate(DataSource dataSource) 方法

java 复制代码
 @Bean(name = "jdbcTemplate")
    // Spring 在调用这个方法的时候会自动给我们传递过来一个dataSource 对象。
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        //jdbcTemplate.setDataSource(dataSource);
        jdbcTemplate.setDataSource(getDataSource());  // 一般是直接调用上面那个

        // 设置好后,返回给Spring 管理
        return jdbcTemplate;
    }

配置事务管理上的配置信息:使用:public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource)

java 复制代码
@Bean(name = "txManager")
    // 事务上的管理
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);

        // 设置好后,返回给Spring 管理
        return dataSourceTransactionManager;
    }

完整的配置文件的信息的编写:

kotlin 复制代码
package com.rainbowsea.bank;


import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration // 代替sprint.xml 配置文件,在这个类当中完成配置
@ComponentScan("com.rainbowsea.bank")  // 组件扫描
@EnableTransactionManagement // 开启事务
public class Spring6Config {


    // 设置数据源信息配置
    /*
    Spring 框架,看到这个@Bean注解后,会调用这个被标注的方法,这个方法的返回值式一个Java对象,
    这个Java对象会自动纳入IOC容器管理,返回的对象就是Spring容器当中的一个Bean了
    并且这个Bean的名字式:dataSource
     */
    @Bean(name = "dataSource")
    public DruidDataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
        dataSource.setUsername("root");
        dataSource.setPassword("MySQL123");

        // 设置好后,返回给Spring 管理
        return dataSource;
    }


    @Bean(name = "jdbcTemplate")
    // Spring 在调用这个方法的时候会自动给我们传递过来一个dataSource 对象。
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        //jdbcTemplate.setDataSource(dataSource);
        jdbcTemplate.setDataSource(getDataSource());  // 一般是直接调用上面那个

        // 设置好后,返回给Spring 管理
        return jdbcTemplate;
    }

    @Bean(name = "txManager")
    // 事务上的管理
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);

        // 设置好后,返回给Spring 管理
        return dataSourceTransactionManager;
    }

}

测试运行:

异常去了,再进行转账,测试是否成功。

csharp 复制代码
public class SpringTxTest {

    @Test
    public void testNoXml() {
        // Spring6Config.class 对应上的配置类
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
        AccountService accountService = applicationContext.getBean("AccountServicelmpl", AccountService.class);

        try {
            accountService.transfer("act-001","act-002",10000);
            System.out.println("转账成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5. 声明式事务之XML实现方式

首先添加相关依赖:记得添加aspectj的依赖:

pom.xml 当中配置相关的 jar 包

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>spring6-014-tx-bank-xml</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>


    <repositories>
        <!--        spring 的版本仓库-->
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


    <dependencies>
        <!--        spring context 依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!--        spring aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--spring jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--spring aspects依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

        <!--德鲁伊连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.13</version>
        </dependency>



        <!-- junit4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>


        <!--@Resource注解-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>

    </dependencies>

</project>

dao 包下的类:

java 复制代码
package com.rainbowsea.bank.dao;

import com.rainbowsea.bank.pojo.Account;

public interface AccountDao {

    /**
     * 根据账号查询账号信息
     * @param actno
     * @return
     */
    Account selectByActno(String actno);


    /**
     * 更新账号信息
     * @param account
     * @return
     */
    int update(Account account);


    /**
     * 保存账户信息
     * @param act
     * @return
     */
    int insert(Account act);
}

bank.dao.impl 包下

java 复制代码
package com.rainbowsea.bank.dao.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;


@Component(value = "accountDaoImpl")
public class AccountDaoImpl implements AccountDao {


    @Resource(name = "jdbcTemplate")  // 该jdbcTemplate 已经纳入了Spring ICO 容器当中管理了,可以用@Resource根据
    // 名称进行 非简单类型的 set 注入赋值
    private JdbcTemplate jdbcTemplate;


    @Override
    public Account selectByActno(String actno) {

        String sql = "select actno,balance from t_act where actno = ?";

        // 查询
        Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);

        return account;
    }

    @Override
    public int update(Account account) {
        String sql = "update t_act set balance = ? where actno = ?";
        int count = jdbcTemplate.update(sql, account.getBalance(), account.getActno());
        return count;
    }

    @Override
    public int insert(Account act) {
        String sql = "insert into t_act(balance,actno) values(?,?)";
        int count = jdbcTemplate.update(sql,  act.getBalance(),act.getActno());
        return count;
    }
}

pojo 包下的类

arduino 复制代码
package com.rainbowsea.bank.pojo;

public class Account {

    private String actno;
    private Double balance;


    public Account() {
    }

    public Account(String actno, Double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "actno='" + actno + ''' +
                ", balance=" + balance +
                '}';
    }


    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }
}

service 包下

java 复制代码
package com.rainbowsea.bank.service;


import com.rainbowsea.bank.pojo.Account;

/**
 * 业务接口
 * 事务就是在这个接口下控制的
 */
public interface AccountService {


    /**
     * 转账业务方法
     * @param fromActno 从这个账户转出
     * @param toActno 转入这个账号
     * @param money 转账金额
     */
    void transfer(String fromActno, String toActno,double money);


}

bank.service.impl 包下的类

java 复制代码
package com.rainbowsea.bank.service.impl;

import com.rainbowsea.bank.dao.AccountDao;
import com.rainbowsea.bank.pojo.Account;
import com.rainbowsea.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;


@Service(value = "accountServicelmpl")
public class AccountServicelmpl implements AccountService {



    @Resource(name = "accountDaoImpl") // accountDaoImpl 已经被纳入了Spring IOC 容器管理了
    // 所以可以使用 @Resource 进行非简单类型的 set 注入赋值
    private AccountDao accountDao;


    // 控制事务: 因为在这个方法中要完成所有的转账业务
    @Override
    public void transfer(String fromActno, String toActno, double money) {

        // 第一步:开启事务

        // 第二步:执行核心业务逻辑

        // 查询转出账号的余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);

        if (fromAct.getBalance() < money) {
            throw new RuntimeException("余额不足,转账失败");
            // 第三步:回滚事务
        }

        // 余额充足
        Account toAct = accountDao.selectByActno(toActno);

        // 将内存中两个对象的余额先修改一下
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);


        // 数据库更新
        int count = accountDao.update(fromAct);

        // 模拟异常
        //String s = null;
        //s.toString();

        count += accountDao.update(toAct);

        if (count != 2) {
            throw new RuntimeException("转账失败,联系银行");
            // 第三步回滚事务
        }

        // 第三步:如果执行业务流程过程中,没有异常,提交事务
        // 第四五:如果执行业务流程过程中,有异常,回滚事务

    }




}

Spring.xml 配置文件如下:记得添加aop的命名空间。

xml 复制代码
<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--    组件扫描-->
    <context:component-scan base-package="com.rainbowsea.bank"></context:component-scan>

<!--    配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!--        注意是:driverClassName 才是简单类型,进行赋值-->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring6"></property>
        <property name="username" value="root"></property>
        <property name="password" value="MySQL123"></property>
    </bean>

<!--    配置JdbcTemplate 交给 Spring IOC容器管理-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

<!--    配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

<!--    配置通知,具体的增强代码-->
<!--    注意:在通知当中要关联事务管理器-->
    <tx:advice id="txAdvice" transaction-manager="txManager">
<!--        配置通知相关属性-->
        <tx:attributes>
<!--            之前所讲的所有的事务属性都可以在以下标签当中配置-->
<!--             method name = "transfter 是"execution(* com.rainbowsea.bank..*(..))" 的具体的方法名"-->
            <tx:method name="transfer" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<!--           method name = save* 是 "execution(* com.rainbowsea.bank..*(..))" 包下的所有模糊方法-->
            <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="modify*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>

            <!--           method name = save* 是 "execution(* com.rainbowsea.bank..*(..))" 是包下的所有模糊方法
            ,并且仅仅只是查,提高查询效率-->
            <tx:method name="query*" read-only="true"/>
            <tx:method name="select*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <tx:method name="get*" read-only="true"/>
        </tx:attributes>
    </tx:advice>

<!--    配置切面-->
    <aop:config>
<!--        切点-->
        <aop:pointcut id="txPointcut" expression="execution(* com.rainbowsea.bank..*(..))"/>
<!--        切面 = 通知 + 切点-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"></aop:advisor>
    </aop:config>

</beans>

运行测试:

运行测试,没有异常,是否转账成功

java 复制代码
package com.rianbowsea.spring6.test;

import com.rainbowsea.bank.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class BankTxTest {

    @Test
    public void testNoAnnotation() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spirng6.xml");
        AccountService accountService = applicationContext.getBean("accountServicelmpl", AccountService.class);
        try {
            accountService.transfer("act-001","act-002",10000.0);
        } catch (Exception e) {
            System.out.println("转账失败");
            e.printStackTrace();
        }
    }
}

6. 总结:

  1. 运行Spring 进行事务处理

    1. 基于注解方式
    2. 基于XML配置方式
  2. 事务上的理解

  3. 事务属性上的配置:

    1. 事务的传播行为
    2. 事务的隔离级别
    3. 事务的超时设置:超时设置是以最后一个 DML 语句的时间进行计时的(不包括最后一条DML语句后面的,不是 DML语句的业务上处理的运行的时间)
    4. 只读事务上的设置,提高查询效率
    5. 设置定义哪些异常回滚事务,不回滚事务
  4. 事务全注解式开发

  5. 声明事务之xml 实现方式

  6. 注意:在Spirng 当中,使用applicationContext.getBean(当中的,xxx.class) 要于返回值类型一致,不然会报类型不一致上的错误。如下:


相关推荐
程序猿小D13 分钟前
[附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+jsp实现的个人财务管理系统,推荐!
java·数据库·mysql·spring·毕业论文·ssm框架·个人财务管理系统
jack_yin42 分钟前
Telegram DeepSeek Bot 管理平台 发布啦!
后端
小码编匠1 小时前
C# 上位机开发怎么学?给自动化工程师的建议
后端·c#·.net
库森学长1 小时前
面试官:发生OOM后,JVM还能运行吗?
jvm·后端·面试
转转技术团队1 小时前
二奢仓店的静默打印代理实现
java·后端
蓝易云1 小时前
CentOS 7上安装X virtual framebuffer (Xvfb) 的步骤以及如何解决无X服务器的问题
前端·后端·centos
钢铁男儿1 小时前
C# 接口(什么是接口)
java·数据库·c#
丶小鱼丶1 小时前
排序算法之【归并排序】
java·排序算法
上上迁1 小时前
分布式生成 ID 策略的演进和最佳实践,含springBoot 实现(Java版本)
java·spring boot·分布式
永日456701 小时前
学习日记-spring-day42-7.7
java·学习·spring