百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

七、Spring 事务

xsobi 2024-12-14 15:44 1 浏览

一、概述


什么是事务?


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


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


事务处理的四个过程:


第一步:开始事务


第二步:执行业务代码


第三步:如果业务代码未出现异常,则提交事务


第四步:如果业务代码出现异常,则回滚事务


事务的四个特特征:


原子性(A):事务是最小的工作单元,不可再分


一致性(C):事务要么同时成功,要么同时失败


隔离性(I):事务与事务这间是相互隔离的,不可以相互干扰


持久性(D):持久性是事务结束的标志


接下来我们以简单的示例来说明事务的处理,比如,我们有两个账户:act-001,act-002,它们之间相互进行转账操作,必须要保证一方的账户的扣减与和另一方账户的增加同时成功或同时失败。


示例中数据表结构如下:


CREATE TABLE `t_act` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `actno` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '',
  `balance` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


我们新建工程,添加如下依赖


<dependencies>
    <!-- spring context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.1.9</version>
    </dependency>
    <!-- spring jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.1.9</version>
    </dependency>
    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <!-- druid -->
    <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>
    <!-- junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>


新增jdbc的配置文件:jdbc.properties


jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root


准备与数据库表相对应的实体类:Account


public class Account {
    private String actno;
    private String balance;

    public Account() {
    }

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

    public String getActno() {
        return actno;
    }

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

    public String getBalance() {
        return balance;
    }

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

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


编写Dao接口:AccountDao


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

    /**
     * 更新账户信息
     * @param act
     * @return
     */
    int update(Account act);
}


编写Dao实现:AccountDaoImpl


@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 rows = jdbcTemplate.update(sql,act.getBalance(),act.getActno());
        return rows;
    }
}


编写业务接口:AccountService


public interface AccountService {
    void transfer(String fromActno, String toActno, double money);
}


编写业务实现:AccountServiceImpl


@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Resource(name = "accountDao")
    private AccountDao accountDao;
    @Override
    public void transfer(String fromActno, String toActno, double money) {
        Account fromAccount = accountDao.selectByActno(fromActno);
        // 判断转账的账户金额是否充足
        if (fromAccount.getBalance() < money) {
            throw new RuntimeException("转账金额不足");
        }
        // 账户余额充足,执行转账操作
        Account toAccount = accountDao.selectByActno(toActno);
        fromAccount.setBalance(fromAccount.getBalance() - money);
        toAccount.setBalance(toAccount.getBalance() + money);
        int rows = accountDao.update(fromAccount);
        rows += accountDao.update(toAccount);
        if (rows != 2) {
            throw new RuntimeException("转账失败");
        }
    }
}


Spring配置文件


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 引入外部jdbc配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

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

    <!-- dataSource配置 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

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

</beans>


测试类:


@Test
public void testTransfer() {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
    AccountService as = context.getBean("accountService", AccountService.class);
    try {
        as.transfer("act-001", "act-002", 1000D);
        System.out.println("转账成功");
    } catch (Exception e) {
        e.printStackTrace();
    }
}


像上面这样,程序正常执行是没有问题的,如果我们模拟一下在转账过程中出现了异常,一方的账户扣减了金额,另一份的账户没有增加金额,这个时候就出现了无故数据丢失!


@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Resource(name = "accountDao")
    private AccountDao accountDao;
    @Override
    public void transfer(String fromActno, String toActno, double money) {
        Account fromAccount = accountDao.selectByActno(fromActno);
        // 判断转账的账户金额是否充足
        if (fromAccount.getBalance() < money) {
            throw new RuntimeException("转账金额不足");
        }
        // 账户余额充足,执行转账操作
        Account toAccount = accountDao.selectByActno(toActno);
        fromAccount.setBalance(fromAccount.getBalance() - money);
        toAccount.setBalance(toAccount.getBalance() + money);
        int rows = accountDao.update(fromAccount);

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

        rows += accountDao.update(toAccount);
        if (rows != 2) {
            throw new RuntimeException("转账失败");
        }
    }
}


要解决上面这种不一致性的问题,我们就要使用事务,那Spring是如何支持事务的?


二、Spring事务


在Spring中实现事务有两种方式


方式一:编程式事务,通过编写代码的方式来实现事务


方式二:声明式事务,它又有两种:基于注解(常用);基于xml


Spring事务管理API


Spring对事务的管理底层是基于AOP的,采用AOP的方式进行封装,这的核心接口与实现类如下所示:


接口:PlatformTransactionManager


实现类:


  • DataSoruceTransactionManager 支持JdbcTemplate,MyBatis,Hibernate等事务管理
  • JtaTransactionManager 支持分布式事务管理


声明式事务管理注解实现


第一步:在Spring配置文件中配置事务管理器


<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>


第二步:Spring配置文件中引入tx命名,开启“事务注解驱动”


<!-- 开启事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>


第三步:在需要注解的地方使用@Transactional


如果我们把这个注解添加到类上,则这个类中的所有方法都会有事务管理,如果只是在某个方法上添加这个注解,则只有这个特定的方法有事务管理


@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Resource(name = "accountDao")
    private AccountDao accountDao;
    
    @Transactional // 事务注解
    @Override
    public void transfer(String fromActno, String toActno, double money) {
        Account fromAccount = accountDao.selectByActno(fromActno);
        // 判断转账的账户金额是否充足
        if (fromAccount.getBalance() < money) {
            throw new RuntimeException("转账金额不足");
        }
        // 账户余额充足,执行转账操作
        Account toAccount = accountDao.selectByActno(toActno);
        fromAccount.setBalance(fromAccount.getBalance() - money);
        toAccount.setBalance(toAccount.getBalance() + money);
        int rows = accountDao.update(fromAccount);

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

        rows += accountDao.update(toAccount);
        if (rows != 2) {
            throw new RuntimeException("转账失败");
        }
    }
}


此时就在transfer这个方法上添加了事务管理。当再次执行测试程序进行转账操作时,最终出现异常后数据没有变化不存在了数据丢失的情况。说明事务已经起效果了。


事务相关属性说明


@Transactional源码如下


@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 {};
}


从上面的源码中可以看到几个重要的属性:


事务的传播性为:propagation


事务的隔离级别:isolation


事务超时:timeout


只读事务:readOnly


配置回滚异常:rollbackFor


配置不回滚异常:noRollbackFor


事务传播行为


什么是事务的传播行为呢?


这个就是接在方法调用时如何对事务进行传递,比如:service中有两个方法a(),b(),它们都有事务,当a()方法执行时调用了b()方法事务如何传递的,是合并成一个事务还是新开启一个事务,这就些是通过事务的传播行为来控制的。


事务传播行为在Spring框架中定义为一个枚举类


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;
    }
}


从上面可以看到有七种传播行为:


REQUIRED:支持当前事务,如果当前不存在事务就会新建一个事务(没有就新建,有则加入)


SUPPORTS:支持当前事务,如果当前不存在事务则会以非事务的方式执行(有则加入,没有就不不要事务了)


MANDATORY:必须运行在一个事务当中,如果当前没有事务则会抛出异常(有则加入,没有就抛异常)


REQUIRES_NEW:开启一个新事务,如果当前已有事务,则把这个事务挂起,不管有没有事务这个时候都会开启一个新的事务,事务与事务之间没有嵌套关系,前面的事务会被 挂起。


NOT_SUPPORTED:以非事务的方式运行,如果当前有事务则挂起当前事务。


NEVER:以非事务方式运行,如果有事务存在则抛出异常


NESTED:如果当前正有一个事务在进行中,则该方法应该运行在一个嵌套式事务中,被嵌套的事务独立于外层事务进行提交或回滚。如果外层事务不存在,和REQUIRED一样。


事务隔离级别


事务隔离级别相当于两个房间之间的那堵墙,如果隔离级别越高这个墙则越厚,隔离得更彻底。


数据库读取数据的三大问题:


脏读:读取到了执行后还没有提交到数据库的记录


不可重复读:同一事务当中第一次读与第二次读到的数据不一样


幻读:读到的数据是假的


事务的四个隔离级别:


读未提交:READ_UNCOMMITTED,这会存在脏读问题,可能读到还没有提交的数据


读已提交:READ_COMMITTED,可以解决脏读,但是有不可重复读的问题


可重复读:REPEATABLE_READ,可以解决不可重复读的问题,只要当前事务不结束,相同的查询条件每次读到的结果都是一样的,但是会存在幻读的问题


序列化:SERIALIZABLE,可以解决幻读的问题,事务排队执行,不支持并发。效率低下


在Spring中隔离级别也是定义为一个枚举的


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;
    }
}


事务超时


事务超时,默认值是-1,表示没有时间限制,永不超时。


@Transactional(timeout = 10) 如我们配置为这个样子表示设置事务的超时时间为10秒,超过这个时间DML语句还没有执行完成则会回滚事务。


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


@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    accountDao.insert(act);
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}


上面这个虽然是设置了事务超时时间为10秒,但是在DML的insert语句执行完后,后面休眠的15秒是不计入超时时间的。


@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    accountDao.insert(act);
}


上面这个一定会超时,因为在超执行最后一个DML的insert语句前就休眠了15秒,这个超过了事务设置的超时时间10秒。


只读事务


@Transactional(readOnly = true)


当前事务设置为只读事务,在这个事务执行过程中只允许select语句执行,delete,insert,update都不可以执行。


设置这个属性的作用:启动Spring的优化策略,提高select语句的执行效率


如果这个事务不进行增册改操作,建议加上只读事务。


哪些异常回滚


@Transactional(rollbackFor = RuntimeException.class)


表示只有发生了RuntimeException异常或这个异常的子类异常时才回滚


哪些异常不回滚


@Transactional(noRollbackFor = NullPointerException.class)


表示当发生了NullPointerException或这个异常的子类异常不回滚,其它的异常会回滚


三、事务全注解开发


使用配置类代替配置文件


@Configuration  // 配置类
@ComponentScan("com.xiaoxie")
@EnableTransactionManagement    // 开启事务管理
public class TxConfig {

    @Bean
    public DataSource getDataSource() {
        // 读取jdbc.properties中的配置信息,创建数据源对象
        Properties prop = new Properties();
        try {
            prop.load(TxConfig.class.getClassLoader().getResourceAsStream("jdbc.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(prop.getProperty("jdbc.driver"));
        dataSource.setUrl(prop.getProperty("jdbc.url"));
        dataSource.setUsername(prop.getProperty("jdbc.username"));
        dataSource.setPassword(prop.getProperty("jdbc.password"));
        return dataSource;
    }

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

    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}


在需要使用事务的地方不审一样使用@Transactional注解即可


四、声明事务之xml实现


这种方式使用的很少,可以了解一下


实现的步骤:


第一步:配置事务管理器


第二步:配置通知


第三步:配置切面


由于需要使用到aop,所以添加如下依赖


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


Spring的配置文件


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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 https://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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 加载外部配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!-- 组件扫描 -->
    <context:component-scan base-package="com.xiaoxie"/>

    <!-- 数据源配置 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 配置jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 配置事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置通知 -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="save" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="update" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="delete" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="transfer" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
        </tx:attributes>
    </tx:advice>
    
    <!-- 配置切面 -->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="txPointcut" expression="execution(* com.xiaoxie.service.impl.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>
</beans>


这个时候匹配到的切点及配置的通知类上会自动加上事务,所以代码中的注解就不用写了

相关推荐

好用的云函数!后端低代码接口开发,零基础编写API接口

前言在开发项目过程中,经常需要用到API接口,实现对数据库的CURD等操作。不管你是专业的PHP开发工程师,还是客户端开发工程师,或者是不懂编程但懂得数据库SQL查询,又或者是完全不太懂技术的人,通过...

快速上手:Windows 平台上 cURL 命令的使用方法

在工作流程中,为了快速验证API接口有效性,团队成员经常转向直接执行cURL命令的方法。这种做法不仅节省时间,而且促进了团队效率的提升。对于使用Windows系统的用户来说,这里有一套详细...

使用 Golang net/http 包:基础入门与实战

简介Go的net/http包是构建HTTP服务的核心库,功能强大且易于使用。它提供了基本的HTTP客户端和服务端支持,可以快速构建RESTAPI、Web应用等服务。本文将介绍ne...

#小白接口# 使用云函数,人人都能编写和发布自己的API接口

你只需编写简单的云函数,就可以实现自己的业务逻辑,发布后就可以生成自己的接口给客户端调用。果创云支持对云函数进行在线接口编程,进入开放平台我的接口-在线接口编程,设计一个新接口,设计和配置好接口参...

极度精神分裂:我家没有墙面开关,但我虚拟出来了一系列开关

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:iN在之前和大家说过,在iN的家里是没有墙面开关的。...

window使用curl命令的注意事项 curl命令用法

cmd-使用curl命令的注意点前言最近在cmd中使用curl命令来测试restapi,发现有不少问题,这里记录一下。在cmd中使用curl命令的注意事项json不能由单引号包括起来json...

Linux 系统curl命令使用详解 linuxctrl

curl是一个强大的命令行工具,用于在Linux系统中进行数据传输。它支持多种协议,包括HTTP、HTTPS、FTP等,用于下载或上传数据,执行Web请求等。curl命令的常见用法和解...

Tornado 入门:初学者指南 tornados

Tornado是一个功能强大的PythonWeb框架和异步网络库。它最初是为了处理实时Web服务中的数千个同时连接而开发的。它独特的Web服务器和框架功能组合使其成为开发高性能Web...

PHP Curl的简单使用 php curl formdata

本文写给刚入PHP坑不久的新手们,作为工具文档,方便用时查阅。CURL是一个非常强大的开源库,它支持很多种协议,例如,HTTP、HTTPS、FTP、TELENT等。日常开发中,我们经常会需要用到cur...

Rust 服务器、服务和应用程序:7 Rust 中的服务器端 Web 应用简介

本章涵盖使用Actix提供静态网页...

我给 Apache 顶级项目提了个 Bug apache顶级项目有哪些

这篇文章记录了给Apache顶级项目-分库分表中间件ShardingSphere提交Bug的历程。说实话,这是一次比较曲折的Bug跟踪之旅。10月28日,我们在GitHub上提...

linux文件下载、服务器交互(curl)

基础环境curl命令描述...

curl简单使用 curl sh

1.curl--help#查看关键字2.curl-A“(添加user-agent<name>SendUser-Agent<name>toserver)”...

常用linux命令:curl 常用linux命令大全

//获取网页内容//不加任何选项使用curl时,默认会发送GET请求来获取内容到标准输出$curlhttp://www.baidu.com//输出<!DOCTYPEh...

三十七,Web渗透提高班之hack the box在线靶场注册及入门知识

一.注册hacktheboxHackTheBox是一个在线平台,允许测试您的渗透技能和代码,并与其他类似兴趣的成员交流想法和方法。它包含一些不断更新的挑战,并且模拟真实场景,其风格更倾向于CT...