Spring 事务

什么是Spring事务

Spring事务是指在Spring框架中对于数据库操作的一种支持,它通过对一组数据库操作进行整体控制来保证数据的一致性和完整性。Spring事务可以保证在一组数据库操作执行时,要么所有操作都执行成功,要么所有操作都回滚到之前的状态,从而避免了数据不一致的情况。

Spring 事务实现方式

  • 编程式事务:需要在代码中手动控制事务的开始、提交和回滚等操作
  • 声明式事务:通过配置或注解的方式来控制事务(推荐)

什么是声明式事务

所谓声明式事务,就是通过配置的方式,比如通过配置文件(xml)或者注解的方式,告诉spring,哪些方法需要spring帮忙管理事务,然后开发者只用关注业务代码,而事务的事情spring自动帮我们控制。

比如注解的方式,只需在方法上面加一个@Transactional注解,那么方法执行之前spring会自动开启一个事务,方法执行完毕之后,会自动提交或者回滚事务,而方法内部没有任何事务相关代码,用起来特别的方便。

@Transactional
public void insert(String userName){
    this.jdbcTemplate.update("insert into t_user (name) values (?)", userName);
}

声明式事务的2种实现方式

  1. 配置文件的方式,即在spring xml文件中进行统一配置,开发者基本上就不用关注事务的事情了,代码中无需关心任何和事务相关的代码,一切交给spring处理。
  2. 注解的方式,只需在需要spring来帮忙管理事务的方法上加上 @Transactional 注解就可以了,注解的方式相对来说更简洁一些,都需要开发者自己去进行配置,可能有些同学对spring不是太熟悉,所以配置这个有一定的风险,做好代码review就可以了。

配置文件的方式这里就不讲了,用的相对比较少,我们主要掌握注解的方式如何使用,就可以了。

@Transactional 注解

@Transactional 注解的使用
  • 放在接口上:接口的实现类中所有 public 都被 spring 自动加上事务
  • 放在类上:当前类以及其下无限级子类中所有 pubilc 方法将被 spring 自动加上事务
  • 放在 public 方法上:该方法将被 spring 自动加上事务
@Transactional 注解参数
参数 描述
value 指定事务管理器的bean名称,如果容器中有多事务管理器PlatformTransactionManager,那么你得告诉spring,当前配置需要使用哪个事务管理器
transactionManager 同value,value和transactionManager选配一个就行,也可以为空,如果为空,默认会从容器中按照类型查找一个事务管理器bean
propagation 事务的传播属性
isolation 事务的隔离级别,就是制定数据库的隔离级别
timeout 事务执行的超时时间(秒),执行一个方法,比如有问题,那我不可能等你一天吧,可能最多我只能等你10秒 10秒后,还没有执行完毕,就弹出一个超时异常吧
readOnly 是否是只读事务,比如某个方法中只有查询操作,我们可以指定事务是只读的设置了这个参数,可能数据库会做一些性能优化,提升查询速度
rollbackFor 定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常及其子类异常的时候,spring会让事务回滚。如果不配制,那么默认会在 RuntimeException 或者 Error 情况下,事务才会回滚
rollbackForClassName 同 rollbackFor,只是这个地方使用的是类名
noRollbackFor 定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常的时候,事务不会回滚
noRollbackForClassName 同 noRollbackFor,只是这个地方使用的是类名

事务隔离

什么是事务隔离

事务隔离是指多个事务可以同时访问数据库中的数据,而当多个事务在数据库中并发执行(同时执行)时,数据的一致性可能受到破坏,从而导致数据出现问题。

image-20230830102958479

事务隔离级别

为了解决各种数据库访问的并发性问题(更新丢失、脏读、不可重复读、幻象读),为此数据库提供了 4 种隔离级别

image-20230830103235126

Read uncommitted(未授权读取、读未提交)

如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据

Read committed(授权读取、读提交)

读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行

Repeatable read(可重复读取)

读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务

Serializable(序列化)

提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行

事务隔离级别对并发性能的影响

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大

虽然数据库的隔离级别可以解决大多数问题,但是灵活度较差,为此又提出了悲观锁和乐观锁的概念

案例

我们可以通过一个简单的账户转账业务,来学习 Spring 事务的使用

第①步:创建账户数据库表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `balance` int(0) NULL DEFAULT NULL COMMENT '余额',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;
第②步:Spring 事务配置

在 Maven 工程中的 resources 下创建 transaction_ioc.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:tx="http://www.springframework.org/schema/tx"
       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
       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="transaction"/>

    <!-- 配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!-- 数据库驱动 -->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <!-- 连接数据库URL -->
        <property name="url" value="jdbc:mysql://localhost:3306/binge?characterEncoding=UTF-8&amp;serverTimezone=GMT" />
        <!-- 连接数据库用户名-->
        <property name="username" value="root" />
        <!-- 连接数据库密码 -->
        <property name="password" value="123456" />
    </bean>

    <!-- 配置JDBC 模版-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 配置数据源-->
        <property name="dataSource" ref="dataSource" />
    </bean>

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

    <!-- 注册事务管理器启动-->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>
第③步:创建 AccountService 业务类

在 maven 工程中创建 transaction 包,在包中创建 AccountService 类,如下:

package transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@Component
public class AccountService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    //@Transactional
    public void transfer(String outUserName,String inUserName,int money) {
        //汇款用户余额 = 现有余额 - 所汇金额
        jdbcTemplate.update("update t_account set balance = balance - ? where name = ?",money,outUserName);

        //模拟系统运行时的突发性问题
        //int i = 1 / 0;

        //收款用户余额 = 现有余额 + 所汇金额
        jdbcTemplate.update("update t_account set balance = balance + ? where name = ?",money,inUserName);
    }
    //获取所有用户信息
    public List<Map<String, Object>> userList() {
        return jdbcTemplate.queryForList("SELECT * FROM t_account");
    }
}

这里使用了 Spring JdbcTemplate 来访问数据库,比较简单轻便,就没必要杀鸡用牛刀,动用 MyBatis 框架啦。

第④步:创建 TransactionTest 测试类
package transaction;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.List;
import java.util.Map;

public class TransactionTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("transaction_ioc.xml");

        AccountService accountService = context.getBean(AccountService.class);

        accountService.transfer("张三","李四",1000);

        List<Map<String, Object>> accounts = accountService.userList();

        System.out.println(accounts);
    }
}

最终工程结构如下:

image-20230830111904610

测试
  1. 测试前我们先注释掉 //@Transactional 注解,暂时关闭 Spring 事务功能

  2. 在数据库表中随便创建两个账户,并设置一定数量的余额

image-20230830111739140

  1. 执行 TransactionTest 测试类,查看数据库显示,张三向李四转账成功,张三账户余额减少1000,而李四账户余额增加1000
  2. 打开 int i = 1 / 0; 代码注释,模拟运行时异常,再次执行 TransactionTest 测试类,出现运行时异常,导致转账失败,但是这时查看数据库,发现张三账户余额减少1000,而李四账户余额没有变化。这显然有问题,张三和李四的账户余额数据不一致
  3. 现在打开 @Transactional 注解`,开启 Spring 事务功能,再次执行 TransactionTest 测试类,也出现运行异常,导致转账失败,而这时查看数据库,发现张三和李四账户余额都没有变化,开启 Spring 事务后数据一致性得到保障

总结

  • 项目中业务层方法在处理某些业务逻辑时,可以会多次操作数据库,如果过程中出现异常,就有可能导致数据库数据不一致问题
  • 通过开启 Spring 事务支持,可以确保对数据库操作的整体性和一致性
posted @ 2023-08-30 11:37  Binge-和时间做朋友  阅读(36)  评论(0编辑  收藏  举报