spring -第十四章 spring事务

1.概述

在学习spring事务前,先简单回忆一下事务的一些内容:
什么是事务?

一个事务可以包含多条sql语句,并且这些语句要么同时成功,要么同时失败。
事务的处理流程?

  1. 开启事务
  2. 执行事务内语句
  3. 提交事务(如果没异常)
  4. 回滚事务(如果有异常)

事务四特性?

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

2.spring对事务的支持

2.1概述

2.1.1实现方式

要在spring中实现事务有两种方式

  • 编程式事务:编写代码手动的开启、提交、回滚事务。
  • 声明式事务:使用注解或xml配置文件进行事务管理。

2.1.2事务管理API

spring基于AOP进行封装,专门开发了一套用于管理事务的API,这套API的核心接口是PlatformTransactionManager.

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

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

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

2.2全注解实现声明式事务

接下来将用一个案例演示如何使用全注解的方式来实现声明式事务的使用。

我们模拟用事务控制两个账户间的转账行为。

所用表和数据如下:

2.2.1依赖包导入

先导入我们需要的依赖包:

复制代码
<!--        spring核心包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.1.12</version>
        </dependency>
  
<!--        spring数据库相关包,我们使用jdbcTemplate操纵数据库-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.1.12</version>
        </dependency>
  
<!--        mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
  
<!--        连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.23</version>
        </dependency>
  
<!--        需要使用其中的@Resource注解,方便属性注入-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>
  
<!--        单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

2.2.2相关类准备

先准备pojo实体类,如下:

java 复制代码
package org.example.pojo;  
  
public class Account {  
    private String actno;  
    private Double balance;  
  
    @Override  
    public String toString() {  
        return "Account{" +  
                "actno='" + actno + '\'' +  
                ", balance=" + balance +  
                '}';  
    }  
  
    public Account() {  
    }  
  
    public Account(String actno, Double balance) {  
        this.actno = actno;  
        this.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;  
    }  
}

Dao层接口

java 复制代码
package org.example.dao;  
  
import org.example.pojo.Account;  
  
public interface AccountDao {  
  
    /**  
     * 根据账号查询余额  
     * @param actno  
     * @return  
     */  
    Account selectByActno(String actno);  
  
    /**  
     * 更新账户  
     * @param act  
     * @return  
     */  
    int update(Account act);  
  
}

Dao层实现类

因为是使用JdbcTemplate来进行数据库操作,所以相较mybatis要额外编写Dao层的实现类。

java 复制代码
package org.example.dao.Impl;  
  
import jakarta.annotation.Resource;  
import org.example.dao.AccountDao;  
import org.example.pojo.Account;  
import org.springframework.stereotype.Repository;  
  
@Repository("accountDao")  
public class AccountDaoImpl implements AccountDao {  
  
    @Resource(name = "jdbcTemplate")  
    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 act) {  
        String sql = "update t_act set balance = ? where actno = ?";  
        int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());  
        return count;  
    }  
}
  • 我们会使用jdbcTemplate对象中的方法来完成对数据库的操作
  • jdbcTemplate对象在配置类中创建,并用@Resource注解注入到Dao层实现类属性中

service层接口

java 复制代码
package org.example.service;  
  
public interface AccountService {  
  
    /**  
     * 转账  
     */  
    void transfer(String fromActno, String toActno, double money);  
}

只有一个转账的方法
service层实现类

java 复制代码
package org.example.service.Impl;  
  
import jakarta.annotation.Resource;  
import org.example.dao.AccountDao;  
import org.example.pojo.Account;  
import org.example.service.AccountService;  
import org.springframework.stereotype.Service;  
  
@Service("accountService")  
public class AccountServiceImpl implements AccountService {  
  
    @Resource(name = "accountDao")  
    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);  
        count += accountDao.update(toAct);  
        if (count != 2) {  
            throw new RuntimeException("转账失败,请联系银行");  
        }  
    }  
}

2.2.3配置类编写

既然要使用全注解的方式进行实现,那么我们就要编写配置类来代替配置文件。

代码如下:

java 复制代码
package org.example.conf;  
  
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  
@ComponentScan("org.example")  
@EnableTransactionManagement  
public class Spring6Config {  
//    配置数据源  
    @Bean  
    public DataSource getDataSource(){  
        DruidDataSource dataSource = new DruidDataSource();  
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");  
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");  
        dataSource.setUsername("root");  
        dataSource.setPassword("15987818261");  
        return dataSource;  
    }  
//    获取jdbcTemplate对象  
    @Bean(name = "jdbcTemplate")  
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){  
        JdbcTemplate jdbcTemplate = new JdbcTemplate();  
        jdbcTemplate.setDataSource(dataSource);  
        return jdbcTemplate;  
    }  
//    获取事务管理器对象  
    @Bean  
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){  
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();  
        dataSourceTransactionManager.setDataSource(dataSource);  
        return dataSourceTransactionManager;  
    }  
}
  • 配置数据源,并管理数据源对象
  • 管理jdbcTemplate对象
  • 配置事务管理器
  • 使用@EnableTransactionManagement开启事务管理
  • 配置包扫描路径

2.2.4@Transactional注解开启事务

完成了配置类的编写后我们就可以使用@Transactional注解来开启事务了。

  • @Transactional注解可以添加在方法上,为对应方法开启事务
  • @Transactional注解可以添加在类上,为类在中所有方法开启事务管理
  • 事务管理功能都是在service层开启,在service层实现类上添加@Transactional注解
  • @Transactional注解有很多属性可以完成额外配置。

现在在我们service层的实现类上使用@Transactional注解开启事务管理。

我们现在进行实验,让service层实现类在最后抛出一个异常,查看事务能否正常回滚。

经测试是能够正常回滚的,说明全注解方式下的事务管理成功实现。

2.3事务属性

直接使用@Transactional注解可以完成最简单的事务控制,但其实该注解中提供了很多属性可以完成额外的配置。
重点属性:

  • 事务传播行为:service层方法互相调用时如何划分事务。
  • 事务隔离级别:不同隔离级别可以解决不同的等级的并发问题。
  • 事务超时:可以设置超时时间,只要时限内事务不能执行。
  • 只读事务:事务中只能查看数据,不能修改。
  • 事务回滚限制:指定事务在什么条件下应该回滚或不应该回滚。

2.3.1事务传播行为

事务传播

是指当service层的方法调用另一个service层方法的情况时 ,如何进行事务管理,是两个方法都在一个事务中?还是各自放到一个事务中?或者其他情况。

设置@Transactional注解中的propagation 属性可以设置事务传播行为,该属性接收一个枚举对象。

可以看出有七种设置值,这里假设是使用service层中的方法A调用了方法B,来理解七种值的情况:

  • REQUIRED :如果方法A中已经开启了事务,那么方法B的逻辑也会被放到事务A中。如果方法A没有开启事务,那么B方法自己开启一个事务。【没有就新建,有就加入】
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**

  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**

  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**

  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**

  • NEVER:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**

  • NESTED :如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

总结:

2.3.2事务隔离就级别

可以使用事务注解中的isolation属性来设置事务的隔离级别,其接受的值是一个枚举对象,对应于数据库中的四种隔离级别:

2.3.3事务超时

通过事务注解中的timeout属性来设置事务超时时间,事务执行时间超过该值就会抛出异常。

接收整数,单位秒。-1代表没有限制。

2.3.4只读事务

设置注解中的readOnlt 属性来为true来开启只读事务,开启后对应的事务代码只能运行select相关语句。

并且最重要的是,spring会开启优化策略,来提高这些select语句的查询效率。

2.3.5回滚限制

设置注解中的rollbackFor 属性来指定在发生什么异常时应该回滚

设置注解中的noRollbackFor 属性来指定发生什么异常时不用回滚

两者接收异常类的class对象 ,发生指定的异常以及其子类时都会生效。

如:

java 复制代码
//表示只有发生RuntimeException异常或该异常的子类异常才回滚。 
@Transactional(rollbackFor = RuntimeException.class)
相关推荐
架构师沉默1 天前
别又牛逼了!AI 写 Java 代码真的行吗?
java·后端·架构
DolphinDB1 天前
集成 Prometheus 与 DolphinDB 规则引擎,构建敏捷监控解决方案
数据库
IvorySQL1 天前
PostgreSQL 技术日报 (3月10日)|IIoT 性能瓶颈与内核优化新讨论
数据库·postgresql·开源
Java水解1 天前
微服务架构下Spring Session与Redis分布式会话实战全解析
后端·spring
DBA小马哥2 天前
时序数据库是什么?能源行业国产化替换的入门必看
数据库·时序数据库
后端AI实验室2 天前
我把一个生产Bug的排查过程,交给AI处理——20分钟后我关掉了它
java·ai
凉年技术2 天前
Java 实现企业微信扫码登录
java·企业微信
爱可生开源社区2 天前
某马来西亚游戏公司如何从 SQL Server 迁移至 OceanBase?
数据库
狂奔小菜鸡2 天前
Day41 | Java中的锁分类
java·后端·java ee
hooknum2 天前
学习记录:基于JWT简单实现登录认证功能-demo
java