Spring 声明式事务

1. 声明式事务概念

1.1 编程式事务

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

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

因此,在实际开发中,应该根据具体情况选择使用声明式事务还是编程式事务。

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

编程式事务的实现方式存在一些缺陷:

  1. 细节没有被屏蔽:编写编程式事务的代码需要考虑很多细节,如事务的开启、提交、回滚等,这些细节都需要程序员自己来完成,比较繁琐。

  2. 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用,增加了代码的冗余度和维护成本。

相比之下,声明式事务通过 AOP 技术将事务管理与业务逻辑分离,将事务管理的细节屏蔽起来,使得代码更加简洁、易读、易维护,同时也提高了代码的复用性。

1.2 声明式事务

声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。具体的事务实现由第三方框架来完成,开发者只需要添加配置即可,避免了直接进行事务操作。相比之下,编程式事务需要手动编写代码来管理事务。

使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。在实际开发中,我们可以根据具体情况选择使用注解或 XML 配置来实现声明式事务。

声明式事务的优点在于可以将事务管理的细节屏蔽起来,使得代码更加简洁、易读、易维护,同时也提高了代码的复用性。此外,声明式事务还可以通过 AOP 技术实现,使得事务管理与业务逻辑分离,更加符合面向对象的设计原则。

1.3 Spring事务管理器

1.Spring声明式事务对应依赖

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

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

  • spring-orm:包含其他持久层框架的事务管理器实现类,如 HibernateTransactionManager、JpaTransactionManager 等。

2. Spring 声明式事务对应事务管理器接口

DataSourceTransactionManager 是 Spring 框架中用于管理 JDBC 数据源事务的事务管理器实现类,它提供了以下主要方法:

  1. doBegin():开启事务。

  2. doSuspend():挂起事务。

  3. doResume():恢复挂起的事务。

  4. doCommit():提交事务。

  5. doRollback():回滚事务。

2 基于注解的声明式事务

2.1 准备工作

依赖

<dependencies>
    <!-- Spring Context 依赖 -->
    <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>
    
    <!-- Spring Test 依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>6.0.6</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Jakarta Annotation API 依赖 -->
    <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>
    
    <!-- Spring AOP 依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>6.0.6</version>
    </dependency>
    
    <!-- Spring AspectJ 依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>6.0.6</version>
    </dependency>
</dependencies>
外部配置文件
url=jdbc:mysql://localhost:3306/studb
driver=com.mysql.cj.jdbc.Driver
username=root
password=root
spring 配置文件
@Configuration
@ComponentScan("com.csi")
@PropertySource("classpath:jdbc.properties")
public class JavaConfig {
    
    @Value("${driver}")
    private String driver;
    
    @Value("${url}")
    private String url;
    
    @Value("${username}")
    private String username;
    
    @Value("${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;
    }
    
    // 配置 JdbcTemplate
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}
dao/service
@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
public class StudentService {
    
    @Autowired
    private StudentDao studentDao;
    
    public void changeInfo() {
        studentDao.updateAgeById(100, 1);
        System.out.println("-----------");
        studentDao.updateNameById("test1", 1);
    }
}
测试
@SpringJUnitConfig(JavaConfig.class)
public class TxTest {
    
    @Autowired
    private StudentService studentService;
    
    @Test
    public void testTx() {
        studentService.changeInfo();
    }
}

2.2 基本事务控制

1.配置事务管理器

/**
 * projectName: com.csi.config
 *
 * description: 数据库和连接池配置类
 */
@Configuration
@ComponentScan("com.csi")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {
    
    /**
     * 实例化 dataSource 加入到 IOC 容器
     * @param url
     * @param driver
     * @param username
     * @param password
     * @return
     */
    @Bean
    public DataSource dataSource(@Value("${url}") String url,
                                 @Value("${driver}") String driver,
                                 @Value("${username}") String username,
                                 @Value("${password}") String password) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
    
    /**
     * 实例化 JdbcTemplate 对象,需要使用 IOC 中的 DataSource
     * @param dataSource
     * @return
     */
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    
    /**
     * 装配事务管理实现对象
     * @param dataSource
     * @return
     */
    @Bean
    public TransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
2. 使用声明事务注解 @Transactional
/**
 * projectName: com.csi.service
 */
@Service
public class StudentService {
    
    @Autowired
    private StudentDao studentDao;
    
    @Transactional
    public void changeInfo() {
        studentDao.updateAgeById(100, 1);
        System.out.println("-----------");
        int i = 1 / 0;
        studentDao.updateNameById("test1", 1);
    }
}
3. 测试事务
/**
 * projectName: com.csi.test
 *
 * description:
 */
//@SpringJUnitConfig(locations = "classpath:application.xml")
@SpringJUnitConfig(classes = DataSourceConfig.class)
public class TxTest {
    
    @Autowired
    private StudentService studentService;
    
    @Test
    public void testTx() {
        studentService.changeInfo();
    }
}

2.3 事务属性:只读

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉 数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化
设置方式
// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)
用法举例
在类级别 @Transactional 注解中设置只读,这样类中所有 的查询方法都不需要设置@Transactional 注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共置即可。
然后在这个基础上,对增删改方法设置 @Transactional 注解 readOnly 属性为 false
@Service
@Transactional(readOnly = true)
public class EmpService {
    
    // 为了便于核对数据库操作结果,不要修改同一条记录
    @Transactional(readOnly = false)
    public void updateTwice(……) {
        ……
    }
}

2.4 事务属性:超时时间

事务超时时间是指在事务执行过程中,如果超过了指定的时间,事务就会被回滚并释放资源。这样可以避免因为某些问题导致事务一直占用资源,从而影响其他正常程序的执行。

在 Spring 中,可以通过 `@Transactional` 注解的 `timeout` 属性来设置事务的超时时间,单位为秒。例如:

@Transactional(timeout = 10)
public void someTransactionalMethod() {
    // ...
}

上面的代码表示,如果 `someTransactionalMethod` 方法执行的时间超过了 10 秒,事务就会被回滚并释放资源。

需要注意的是,事务超时时间的设置应该根据实际情况进行调整,不宜设置过长或过短。如果事务执行的时间比较长,可以考虑将事务拆分成多个小事务,或者优化事务执行的代码逻辑,减少事务执行的时间。

2.5 事务属性:事务异常

在 Spring 中,事务的异常处理是通过 `@Transactional` 注解的 `rollbackFor` 和 `noRollbackFor` 属性来实现的。

`rollbackFor` 属性用于指定哪些异常需要回滚事务,可以指定一个或多个异常类型。例如:

@Transactional(rollbackFor = {SQLException.class, IOException.class})
public void someTransactionalMethod() {
    // ...
}

上面的代码表示,如果 `someTransactionalMethod` 方法抛出了 `SQLException` 或 `IOException` 异常,事务就会被回滚。

`noRollbackFor` 属性用于指定哪些异常不需要回滚事务,同样可以指定一个或多个异常类型。例如:

@Transactional(noRollbackFor = {NullPointerException.class, IllegalArgumentException.class})
public void someTransactionalMethod() {
    // ...
}

上面的代码表示,如果 `someTransactionalMethod` 方法抛出了 `NullPointerException` 或 `IllegalArgumentException` 异常,事务不会被回滚。

需要注意的是,如果同时指定了 `rollbackFor` 和 `noRollbackFor` 属性,`noRollbackFor` 属性会覆盖 `rollbackFor` 属性。因此,在使用这两个属性时需要注意避免出现冲突。

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

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

  1. 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
  2. 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
  3. 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
  4. 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。
事务隔离级别设置
package com.csi.service;

import com.csi.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.csi.service
 */
@Service
public class StudentService {

    @Autowired
    private StudentDAO studentDAO;

    /**
     * timeout 设置事务超时时间,单位秒!默认:-1 永不超时,不限制事务时间!
     * rollbackFor 指定哪些异常才会回滚,默认是 RuntimeException 和 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 事务属性:事务传播行为

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

在一个应用程序中,可能会存在多个事务,这些事务之间可能会相互调用,形成一个事务调用链。在这种情况下,就需要考虑事务传播行为的问题,即一个事务方法调用另一个事务方法时,如何处理事务。

举例来说,假设有两个事务方法 A 和 B,方法 A 调用方法 B,那么在方法 B 中应该如何处理事务?是加入方法 A 的事务中,还是开启一个新的事务?如果开启一个新的事务,那么方法 A 和方法 B 就会分别运行在不同的事务中,这可能会导致数据不一致的问题。

因此,事务传播行为就是指在多个事务方法相互调用的情况下,如何处理事务的问题。Spring 提供了多种事务传播行为,可以根据实际情况进行选择。

2. 事务传播行为的类型

Spring 中定义了 7 种事务传播行为,分别是:

- REQUIRED:如果当前存在事务,则加入该事务;否则开启一个新的事务。
- SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式执行。
- MANDATORY:如果当前存在事务,则加入该事务;否则抛出异常。
- REQUIRES_NEW:开启一个新的事务,如果当前存在事务,则挂起该事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起该事务。
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则在该事务中嵌套一个新的事务;否则开启一个新的事务。

3. 事务传播行为的使用

在 Spring 中,可以通过 `@Transactional` 注解的 `propagation` 属性来设置事务传播行为。例如:

@Transactional(propagation = Propagation.REQUIRED)
public void someTransactionalMethod() {
    // ...
}

上面的代码表示,`someTransactionalMethod` 方法的事务传播行为为 REQUIRED。

需要注意的是,不同的事务传播行为适用于不同的场景,需要根据实际业务需求进行选择和调整。同时,事务传播行为的设置也会影响事务的隔离级别和超时时间等属性,需要综合考虑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值