Spring学习之声明式事务(Spring事务学习这一篇足够了)

目录

一、Spring声明式事务

1.1 声明式事务概念

1.1.1 编程式事务

1.1.2 声明式事务

1.1.3 二者的区别:

1.2 Spring事务管理器

1.2.1、Spring声明式事务对应依赖

1.2.2、Spring声明式事务对应事务管理器接口

二、基于注解的声明式事务

2.1 准备项目

2.2 事务添加

2.3 事务属性:只读

2.3.1.  只读介绍

2.3.2.  设置方式

2.3.3.  针对DML动作设置只读模式

2.3.4.  @Transactional注解放在类上

    1.  生效原则

    2.  用法举例

2.4 事务属性:超时时间

2.4.1.  需求

2.4.2.  设置超时时间

2.5 事务属性:事务异常

2.5.1.  默认情况

2.5.2.  设置回滚异常

2.5.3.  设置不回滚的异常

2.6 事务属性:事务隔离级别

2.6.1.  事务隔离级别

2.6.2.  事务隔离级别设置

2.7 事务属性:事务传播行为

2.7.1.  事务传播行为要研究的问题

2.7.2.  propagation属性

2.7.3.  测试

1.  声明两个业务方法

    2.  声明一个整合业务方法

 3.  添加传播行为测试

注意:

4.  其他传播行为值(了解)


一、Spring声明式事务

1.1 声明式事务概念

1.1.1 编程式事务

编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 `PlatformTransactionManager`)来实现编程式事务。

编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。

Connection conn = ...;
  
try {
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    // 核心操作
    // 业务代码
    // 提交事务
    conn.commit();
  
}catch(Exception e){
  
    // 回滚事务
    conn.rollBack();
  
}finally{
  
    // 释放数据库连接
    conn.close();
  
}

编程式的实现方式存在缺陷:

-   细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。

-   代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

1.1.2 声明式事务

声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。

开发者只需要添加配置即可, 具体事务的实现由第三方框架实现,避免我们直接进行事务操作!

使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。

1.1.3 二者的区别:

-   编程式事务需要手动编写代码来管理事务

-   而声明式事务可以通过配置文件或注解来控制事务。

1.2 Spring事务管理器

我们在学习Spring事务的过程中就需要用到Spring事务管理器。

1.2.1、Spring声明式事务对应依赖

    -   spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)

    -   spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager

    -   spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等

1.2.2、Spring声明式事务对应事务管理器接口

    ![](image/image_s2BCX_Qltm.png)

我们现在要使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现!

    DataSourceTransactionManager类中的主要方法:

    -   doBegin():开启事务

    -   doSuspend():挂起事务

    -   doResume():恢复挂起的事务

    -   doCommit():提交事务

    -   doRollback():回滚事务

二、基于注解的声明式事务

2.1 准备项目

pom依赖

<dependencies>
      <!--spring context依赖-->
      <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>6.0.6</version>
      </dependency>

      <!--junit5测试-->
      <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-api</artifactId>
          <version>5.3.1</version>
      </dependency>


      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>6.0.6</version>
          <scope>test</scope>
      </dependency>

      <dependency>
          <groupId>jakarta.annotation</groupId>
          <artifactId>jakarta.annotation-api</artifactId>
          <version>2.1.1</version>
      </dependency>

      <!-- 数据库驱动 和 连接池-->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.25</version>
      </dependency>

      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.2.8</version>
      </dependency>

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

      <!-- 声明式事务依赖-->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-tx</artifactId>
          <version>6.0.6</version>
      </dependency>


      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aop</artifactId>
          <version>6.0.6</version>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aspects</artifactId>
          <version>6.0.6</version>
      </dependency>
    </dependencies>

配置文件jdbc.properties

gedeshidai.driver=com.mysql.jdbc.Driver
gedeshidai.url=jdbc:mysql://localhost:3306/studb?serverTimezone=UTC&characterEncoding=utf8
gedeshidai.username=root
gedeshidai.password=root

Spring配置文件

@Configuration
    @ComponentScan("com.gedeshidai")
    @PropertySource("classpath:jdbc.properties")
    public class JavaConfig {

        @Value("${gedeshidai.driver}")
        private String driver;
        @Value("${gedeshidai.url}")
        private String url;
        @Value("${gedeshidai.username}")
        private String username;
        @Value("${gedeshidai.password}")
        private String password;



        //druid连接池
        @Bean
        public DataSource dataSource(){
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(driver);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            return dataSource;
        }


        @Bean
        //jdbcTemplate
        public JdbcTemplate jdbcTemplate(DataSource dataSource){
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }

    }

dao层

@Repository
    public class StudentDao {
        
        @Autowired
        private JdbcTemplate jdbcTemplate;
        
        public void updateNameById(String name,Integer id){
            String sql = "update students set name = ? where id = ? ;";
            int rows = jdbcTemplate.update(sql, name, id);
        }

        public void updateAgeById(Integer age,Integer id){
            String sql = "update students set age = ? where id = ? ;";
            jdbcTemplate.update(sql,age,id);
        }
    }

service层

@Service
    public class StudentService {
        
        @Autowired
        private StudentDao studentDao;
        
        public void changeInfo(){
            studentDao.updateAgeById(100,1);
            System.out.println("-----------");
            studentDao.updateNameById("test1",1);
        }
    }

测试环境搭建

/**
     * projectName: com.gedeshidai.test
     *
     * description:
     */
    @SpringJUnitConfig(JavaConfig.class)
    public class TxTest {

        @Autowired
        private StudentService studentService;

        @Test
        public void  testTx(){
            studentService.changeInfo();
        }
    }

数据库代码

create database studb;

    use studb;
		CREATE TABLE students (
      id INT PRIMARY KEY,
      name VARCHAR(50) NOT NULL,
      gender VARCHAR(10) NOT NULL,
      age INT,
      class VARCHAR(50)
    );
		INSERT INTO students (id, name, gender, age, class)
    VALUES
      (1, '张三', '男', 20, '高中一班'),
      (2, '李四', '男', 19, '高中二班'),
      (3, '王五', '女', 18, '高中一班'),
      (4, '赵六', '女', 20, '高中三班'),
      (5, '刘七', '男', 19, '高中二班'),
      (6, '陈八', '女', 18, '高中一班'),
      (7, '杨九', '男', 20, '高中三班'),
      (8, '吴十', '男', 19, '高中二班');

错误解决

类文件具有错误的版本 61.0, 应为 52.0

请删除该文件或确保该文件位于正确的类路径子目录中。

当出现这种情况可以去检查自己jdk版本,博主的jdk1.8和这个冲突了,所以改成了jdk17或以上版本。

2.2 事务添加

@Transactional

注释添加位置:方法/类上

方法:当前方法有事务

类上:类下所有的方法都有事务

在JavaConfig.java中,我们需要添加第三方的类进入到IOC容器

在提交事务的方法/类上添加注释:@Transactional,博主这里是在类上添加的。

当在类上添加注释后会,运行测试类会出现如下报错:

2.3 事务属性:只读

2.3.1.  只读介绍

    对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

2.3.2.  设置方式

    // readOnly = true把当前事务设置为只读 默认是false!

    @Transactional(readOnly = true)

2.3.3.  针对DML动作设置只读模式

    会抛出下面异常:

    Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

2.3.4.  @Transactional注解放在类上

    1.  生效原则

        如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。

        对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。

    2.  用法举例

        在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。

        然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。

代码实现:

当我们添加已读,把报错那一行注释后,运行测试结果如下:

这里可能小伙伴会想,既然进行修改的方法加只读不会运行,那么我们只进行查询是不是就可以加只读模式了?

答案当然是:可以的!

但是,既然我们是查询了,也就没必要添加事务了呀!

所以已读模式一般都是添加到类上的,类下的所有方法都有事务,查询方法可以通过再次添加注解,设置为只读模式,提高效率。(@Transactional默认为非只读模式)

2.4 事务属性:超时时间

2.4.1.  需求

    事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

    此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

    概括来说就是一句话:超时回滚,释放资源。

2.4.2.  设置超时时间

如果类上设置事务属性,方法也设置了事务注解!那么方法会不会生效呢?

答案是:不会生效!!!

因为方法上的注解覆盖了类上的注解(直接在方法上设置注解是比在类上的优先级大的)

2.5 事务属性:事务异常

2.5.1.  默认情况

    默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:

@Service
    public class StudentService {

        @Autowired
        private StudentDao studentDao;

        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         */
        @Transactional(readOnly = false,timeout = 3)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! 
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }

2.5.2.  设置回滚异常

    rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException and Error 异常方可回滚!

/**
     * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
     * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
     * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
     */
    @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class)
    public void changeInfo() throws FileNotFoundException {
        studentDao.updateAgeById(100,1);
        //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! 
        new FileInputStream("xxxx");
        studentDao.updateNameById("test1",1);
    }

2.5.3.  设置不回滚的异常

    在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。

    noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!

@Service
    public class StudentService {

        @Autowired
        private StudentDao studentDao;

        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         */
        @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }

2.6 事务属性:事务隔离级别

2.6.1.  事务隔离级别

    数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:

    1.  读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。

    2.  读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。

    3.  可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。

    4.  串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。

        不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。

2.6.2.  事务隔离级别设置

package com.gedeshidai.service;

    import com.gedeshidai.dao.StudentDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Transactional;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;

    /**
     * projectName: com.gedeshidai.service
     */
    @Service
    public class StudentService {

        @Autowired
        private StudentDao studentDao;

        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         * isolation = 设置事务的隔离级别,mysql默认是repeatable read!
         */
        @Transactional(readOnly = false,
                       timeout = 3,
                       rollbackFor = Exception.class,
                       noRollbackFor = FileNotFoundException.class,
                       isolation = Isolation.REPEATABLE_READ)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }

2.7 事务属性:事务传播行为

2.7.1.  事务传播行为要研究的问题

   举例代码:

@Transactional
    public void MethodA(){
        // ...
        MethodB();
        // ...
    }

    //在被调用的子方法中设置传播行为,代表如何处理调用的事务! 是加入,还是新事务等!
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void MethodB(){
        // ...
    }

2.7.2.  propagation属性

@Transactional 注解通过 propagation 属性设置事务的传播行为。它的默认值是:

Propagation propagation() default Propagation.REQUIRED;

    propagation 属性的可选值由 org.springframework.transaction.annotation.Propagation 枚举类提供:

    | 名称                | 含义                        |

    | ----------------- | ------------------------- |

    | REQUIRED &#xA;默认值 | 如果父方法有事务,就加入,如果没有就新建自己独立! |

    | REQUIRES\_NEW     | 不管父方法是否有事务,我都新建事务,都是独立的!  |

2.7.3.  测试

1.  声明两个业务方法
 @Service
        public class StudentService {

            @Autowired
            private StudentDao studentDao;

            /**
             * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
             * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
             * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
             * isolation = 设置事务的隔离级别,mysql默认是repeatable read!
             */
            @Transactional(readOnly = false,
                           timeout = 3,
                           rollbackFor = Exception.class,
                           noRollbackFor = FileNotFoundException.class,
                           isolation = Isolation.REPEATABLE_READ)
            public void changeInfo() throws FileNotFoundException {
                studentDao.updateAgeById(100,1);
                //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
                new FileInputStream("xxxx");
                studentDao.updateNameById("test1",1);
            }
            

            /**
             * 声明两个独立修改数据库的事务业务方法
             */
            @Transactional(propagation = Propagation.REQUIRED)
            public void changeAge(){
                studentDao.updateAgeById(99,1);
            }

            @Transactional(propagation = Propagation.REQUIRED)
            public void changeName(){
                studentDao.updateNameById("test2",1);
                int i = 1/0;
            }

        }
    2.  声明一个整合业务方法
@Service
        public class TopService {

            @Autowired
            private StudentService studentService;

            @Transactional
            public void  topService(){
                studentService.changeAge();
                studentService.changeName();
            }
        }
 3.  添加传播行为测试
@SpringJUnitConfig(classes = AppConfig.class)
        public class TxTest {

            @Autowired
            private StudentService studentService;

            @Autowired
            private TopService topService;

            @Test
            public void  testTx() throws FileNotFoundException {
                topService.topService();
            }
        }
注意:

    在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。

4.  其他传播行为值(了解)

    1.  Propagation.REQUIRED:如果当前存在事务,则加入当前事务,否则创建一个新事务。

    2.  Propagation.REQUIRES\_NEW:创建一个新事务,并在新事务中执行。如果当前存在事务,则挂起当前事务,即使新事务抛出异常,也不会影响当前事务。

    3.  Propagation.NESTED:如果当前存在事务,则在该事务中嵌套一个新事务,如果没有事务,则与Propagation.REQUIRED一样。

    4.  Propagation.SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务方式执行。

    5.  Propagation.NOT\_SUPPORTED:以非事务方式执行,如果当前存在事务,挂起该事务。

    6.  Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常。

    7.  Propagation.NEVER:必须在没有事务的情况下执行,否则抛出异常。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哥的时代

您的打赏是对我最大的支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值