Spring-事务

阅读这篇文章,解释上一个发的Spring-JUnit测试单元一起看


目录

阅读这篇文章,解释上一个发的Spring-JUnit测试单元一起看

[6. 声明式事务](#6. 声明式事务)

[6.1. 事务概念](#6.1. 事务概念)

[6.2. 事务的特性](#6.2. 事务的特性)

[6.3. 编程式事务](#6.3. 编程式事务)

[6.4. 声明式事务](#6.4. 声明式事务)

[6.5. 基于注解的声明式事务](#6.5. 基于注解的声明式事务)

[6.5.1. 准备工作](#6.5.1. 准备工作)

[6.5.2. 测试无事务情况](#6.5.2. 测试无事务情况)

[6.5.3. 加入事务](#6.5.3. 加入事务)

[6.5.3.1. 添加事务配置](#6.5.3.1. 添加事务配置)

[6.5.3.2. 添加事务注解](#6.5.3.2. 添加事务注解)

[6.5.4. @Transactional注解标识的位置](#6.5.4. @Transactional注解标识的位置)

[6.5.5. 事务属性:只读](#6.5.5. 事务属性:只读)

[6.5.6. 事务属性:超时](#6.5.6. 事务属性:超时)

[6.5.7. 事务属性:回滚策略](#6.5.7. 事务属性:回滚策略)

[6.5.8. 事务属性:隔离级别](#6.5.8. 事务属性:隔离级别)

[6.5.9. 事务属性:传播行为](#6.5.9. 事务属性:传播行为)

[6.5.10. 全注解配置事务](#6.5.10. 全注解配置事务)

[6.6. 基于XML的声明式事务](#6.6. 基于XML的声明式事务)

[6.6.1. 场景模拟](#6.6.1. 场景模拟)

[6.6.2. 修改Spring配置文件](#6.6.2. 修改Spring配置文件)


6. 声明式事务

6.1. 事务概念

事务是数据库操作的一个基本单元,它是一系列数据库操作的集合。这些操作被看作是一个整体,具有以下几个特点

  • 完整性:事务中的所有操作被视为一个不可分割的整体。要么所有操作都成功完成并被提交(Commit),要么在遇到问题时全部撤销(Rollback),回到事务开始前的状态。
  • 序列化:事务内的操作按照一定的顺序执行,尽管实际执行过程中可能由于并发等原因交错进行,但逻辑上它们被视为按序执行。

简而言之,事务就像是数据库中的一次"原子"操作,要么全部发生,要么全部不发生,不会出现部分操作生效、部分未生效的情况。


6.2. 事务的特性

事务具备以下四个重要特性,通常以ACID表示

A:原子性(Atomicity)

原子性确保事务中的所有操作要么全部成功完成,要么全部失败回滚。这意味着事务内部的操作是不可分割的,对于外界来说,一个事务看起来就像是一个单一的操作。如果事务在执行过程中遇到任何问题(如系统故障、逻辑错误等),所有已完成的操作都会被撤销,数据库状态回到事务开始前,保证了数据的完整性。

C:一致性(Consistency)

一致性确保事务执行前后,数据库始终处于一致的状态。这意味着事务的执行结果必须使数据库从一个有效的状态转换到另一个有效的状态。无论事务成功提交还是因故回滚,都不会破坏数据库的完整性约束(如主键唯一、外键引用等)和业务规则。

I:隔离性(Isolation)

隔离性保证在并发环境中,多个事务同时执行时,它们之间互不影响。每个事务都好像在独立、隔离的环境中操作数据,无法看到其他事务未提交的中间结果。为实现隔离性,数据库系统通常提供不同的隔离级别(如读未提交、读已提交、可重复读、串行化等),在不同程度上防止脏读、不可重复读、幻读等问题的发生。

D:持久性(Durability)

持久性确保一旦事务成功提交,其所做的更改就会永久保存在数据库中,即使发生系统崩溃、电源故障等情况,这些更改也不会丢失。数据库通过事务日志(Transaction Log)等机制,确保在系统恢复后能够将已提交事务的更改正确地还原到数据库中,保持数据的持久性。


总结:事务是数据库中保证数据完整性和一致性的基础工具,通过原子性、一致性、隔离性和持久性这四个特性,确保了在并发环境下进行复杂数据操作时,数据状态的正确性和可靠性。理解并正确运用事务是进行数据库编程和管理的重要基础。


6.3. 编程式事务

编程式事务是指在编写程序时,由开发者直接使用编程语言(如Java、Python等)调用数据库提供的API或接口,手动控制事务的开启、提交和回滚等操作。这种方式让开发者对事务管理有完全的控制权,但同时也需要处理事务相关的所有细节。

简单说就是:事务功能的相关操作全部通过自己编写代码来实现

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

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

  • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

6.4. 声明式事务

声明式事务是相对于编程式事务而言的一种更为简洁、高级的事务管理方式。它将事务控制的逻辑从具体的业务代码中抽离出来,通过配置文件、注解或其他元数据方式,以声明的方式告诉框架(如Spring、Hibernate等)应该如何管理事务。


声明式事务:

  • 不需自己写事务控制代码 :在使用声明式事务时,开发者无需像编程式事务那样编写开启事务、提交事务、回滚事务以及处理异常的详细代码。取而代之的是,在方法签名、类定义、配置文件等位置添加特定的注解(如Spring框架中的@Transactional)、标签或属性,声明该方法或类需要进行事务管理。
  • 通过配置让框架实现事务管理 :框架会根据这些声明信息,在运行时自动织入(或称为拦截)相应的事务处理逻辑。例如,当一个标注了@Transactional的方法被调用时,Spring会在方法开始前开启事务,方法结束后根据方法执行情况(正常返回还是抛出异常)决定提交或回滚事务。整个过程对业务代码透明,开发者只需关注业务逻辑本身。

声明式事务的优势:

  • 提高开发效率:由于事务管理的代码被框架统一处理,开发者无需关心事务细节,只需关注业务逻辑,简化了编码工作,提高了开发速度。
  • 消除冗余代码:在编程式事务中,事务控制代码往往在多个地方重复出现。声明式事务通过集中配置,避免了这种重复,使代码更加精简、易于维护。
  • 框架优化:成熟的框架(如Spring)在实现声明式事务时,已经充分考虑了实际开发环境中的各种复杂情况,如事务传播行为、隔离级别设定、异常分类处理等,并进行了相应的健壮性、性能优化。使用声明式事务,相当于直接利用了框架提供的成熟解决方案,降低了自行处理事务时可能引入的风险和问题。

总之,声明式事务是一种以配置代替手动编码的方式来管理事务的方法,它极大地简化了事务处理工作,提高了代码质量,是现代企业级应用中广泛采用的事务管理方式。对于初学者来说,理解声明式事务的关键在于认识到其通过声明(如注解)而非直接编写事务控制代码来管理事务,从而减轻开发负担,提升开发效率和代码质量。随着对框架的学习和使用,您会逐渐熟悉如何在实际项目中配置和使用声明式事务。


6.5. 基于注解的声明式事务

6.5.1. 准备工作

用户买书过程演示事务操作


  1. 添加配置

在beans.xml添加配置

复制代码
<!-- 扫描指定包下所有组件并加入IOC容器,实现对注解的自动识别和处理 -->
<context:component-scan base-package="com.sakurapaid.spring6.tx"/>
  1. 创建表

    CREATE TABLE t_book (
    book_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    book_name varchar(20) DEFAULT NULL COMMENT '图书名称',
    price int(11) DEFAULT NULL COMMENT '价格',
    stock int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
    PRIMARY KEY (book_id)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

    insert into t_book(book_id,book_name,price,stock)
    values (1,'Java核心技术卷',80,100),(2,'算法导论',50,100);

    CREATE TABLE t_user (
    user_id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    username varchar(20) DEFAULT NULL COMMENT '用户名',
    balance int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
    PRIMARY KEY (user_id)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

    insert into t_user(user_id,username,balance) values (1,'Sakurapaid',50);

  1. 创建组件

创建BookController

复制代码
package com.sakurapaid.spring6.tx.controller;

import com.sakurapaid.spring6.tx.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

/**
 * 图书控制器类,负责处理图书相关的HTTP请求。
 */
@Controller
public class BookController {

    /**
     * 自动注入的图书服务接口,用于执行图书相关的业务逻辑。
     */
    @Autowired
    private BookService bookService;

    /**
     * 处理购买图书的请求。
     * @param bookId 要购买的图书ID。
     * @param userId 进行购买的用户ID。
     * 该方法没有返回值,购买操作的结果(例如,是否成功,库存是否足够等)会通过其他方式(如异常、日志)呈现。
     */
    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}

创建接口BookService

复制代码
/**
 * 图书服务接口,提供购买图书的功能。
 */
package com.sakurapaid.spring6.tx.service;

public interface BookService {
    /**
     * 购买图书
     * 
     * @param bookId 图书ID,指定要购买的图书。
     * @param userId 用户ID,指定进行购买的用户。
     * 该方法没有返回值,购买操作的结果(例如成功或失败)通过其他方式(如异常处理或日志记录)来处理。
     */
    void buyBook(Integer bookId, Integer userId);
}

创建实现类BookServiceImpl

复制代码
package com.sakurapaid.spring6.tx.service;

import com.sakurapaid.spring6.tx.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 图书服务实现类,提供购买图书的功能。
 */
@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    /**
     * 购买图书。
     * @param bookId 图书ID
     * @param userId 用户ID
     * 该方法会查询图书价格,更新图书库存和用户余额。
     */
    @Override
    public void buyBook(Integer bookId, Integer userId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);
        // 更新图书库存
        bookDao.updateStock(bookId);
        // 更新用户余额
        bookDao.updateBalance(userId, price);
    }
}

创建接口BookDao

复制代码
package com.sakurapaid.spring6.tx.dao;

/**
 * 图书数据访问接口
 */
public interface BookDao {
    
    /**
     * 根据图书ID获取图书价格
     * 
     * @param bookId 图书ID
     * @return 图书价格,返回类型为整数
     */
    Integer getPriceByBookId(Integer bookId);

    /**
     * 更新图书库存
     * 
     * @param bookId 图书ID
     */
    void updateStock(Integer bookId);
    
    /**
     * 更新用户余额
     * 
     * @param userId 用户ID
     * @param price 更新的金额
     */
    void updateBalance(Integer userId, Integer price);
}

创建实现类BookDaoImpl

复制代码
package com.sakurapaid.spring6.tx.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository // 标识为数据库访问层组件
public class BookDaoImpl implements BookDao {

    @Autowired // 自动注入JdbcTemplate
    private JdbcTemplate jdbcTemplate;

    /**
     * 根据书本ID获取价格
     * @param bookId 书本的ID
     * @return 返回对应书本的价格,如果不存在则返回null
     */
    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
    }

    /**
     * 更新书本库存
     * @param bookId 书本的ID
     */
    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(sql, bookId);
    }

    /**
     * 更新用户余额
     * @param userId 用户的ID
     * @param price 扣除的金额
     */
    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance = balance - ? where user_id = ?";
        jdbcTemplate.update(sql, price, userId);
    }
}

项目结构

方便测试,用户余额改为1000


6.5.2. 测试无事务情况

创建测试类

复制代码
package com.sakurapaid.spring6.tx;

import com.sakurapaid.spring6.tx.controller.BookController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:beans.xml")
public class TxByAnnotationTest {
    @Autowired
    private BookController bookController;

    @Test
    public void testBuyBook(){
        bookController.buyBook(1, 1);
    }
}

测试之前的表数据

测试之后的表数据


上面的是正常的案例,现在举例一个错误情况来引出事务

假设用户id为1的用户,购买id为1的图书

用户余额为50,而图书价格为80

购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段

再执行刚才的测试语句

报错了,错误信息简单解释就是

错误信息指出,在执行数据库更新操作时,尝试从t_user表的balance字段中减去一个值(在这个案例中是80),但由于balance字段被定义为BIGINT UNSIGNED类型,即一个无符号的大整数,它的值不能为负。因此,当尝试执行这个减法操作时,数据库抛出了DataIntegrityViolationException异常,导致程序异常终止。

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新 显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败


6.5.3. 加入事务

6.5.3.1. 添加事务配置

在spring配置文件中引入tx命名空间

复制代码
<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"
       
       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">

在Spring的配置文件中添加配置

复制代码
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="druidDataSource"></property>
</bean>

<!--
    开启事务的注解驱动
    通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
-->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager" />

完整的是这样的

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!-- Spring核心 beans 命名空间配置文件 -->
<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"
       
       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">
    
    <!-- 开启Spring组件自动扫描功能,让Spring自动发现和注册组件 -->
    <context:component-scan base-package="com.sakurapaid.spring6.tx"/>
    
    <!-- 引入外部属性文件,并创建数据源对象 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <!-- 配置数据源,使用Druid数据源连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!-- 使用外部属性文件中定义的属性值配置数据源 -->
        <property name="driverClassName" value="${jdbc.driver}"/>
        <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="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 开启基于注解的事务管理 transaction-manager: 如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性-->
    <tx:annotation-driven transaction-manager="transactionManager" />
    <!-- 通过注解@Transactional所标识的方法或类中所有的方法,都会被事务管理器管理事务 -->

</beans>
6.5.3.2. 添加事务注解

因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理

在BookServiceImpl的buybook()添加注解@Transactional

复制代码
package com.sakurapaid.spring6.tx.service;

import com.sakurapaid.spring6.tx.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 图书服务实现类,提供购买图书的功能。
 */
@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    /**
     * 购买图书。
     * @param bookId 图书ID
     * @param userId 用户ID
     * 该方法会查询图书价格,更新图书库存和用户余额。
     */
    @Override
    @Transactional
    public void buyBook(Integer bookId, Integer userId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);
        // 更新图书库存
        bookDao.updateStock(bookId);
        // 更新用户余额
        bookDao.updateBalance(userId, price);
    }
}

重新测试输出

表数据改为初始值

测试类还是报一样的错

但是表数据没有发生变化,因为事务的回滚和初始值是一样的


6.5.4. @Transactional注解标识的位置

@Transactional标识在方法上,则只会影响该方法

@Transactional标识的类上,则会影响类中所有的方法

事务注解是可以写属性值的,简单说有一下几种

  1. 只读:事务被设置为只读时,它只能进行数据的读取操作,不允许执行任何写入操作,如更新或删除。
  2. 超时:事务有一个超时限制,如果在规定的时间内事务没有完成,它将自动回滚到开始前的状态。
  3. 回滚策略:定义了事务在遇到异常时是否应该回滚。可以指定某些异常会导致事务回滚,而其他异常则不会。
  4. 隔离级别:事务的隔离级别决定了它与其他并发事务的隔离程度,防止数据不一致的问题,如脏读、不可重复读和幻读。
  5. 传播行为:描述了当事务方法被另一个事务上下文调用时,事务应该如何传播,例如创建新事务或加入现有事务。

6.5.5. 事务属性:只读

视频例子可以点这个链接 --> 事务属性


①介绍

事务的"只读"属性是一种特殊的设置,用于告诉Spring框架和数据库,当前事务仅包含查询操作,而不会进行数据的增加、删除或修改。这个设置可以让数据库针对查询进行优化,因为数据库知道不需要对数据的写操作进行保护,从而可能提高查询效率。

②使用方式--readOnly

在Spring框架中,可以通过@Transactional注解的readOnly属性来设置一个方法为只读事务。例如,当你调用一个方法并希望它只进行查询操作时,可以这样设置:

复制代码
@Transactional(readOnly = true)
public void someQueryMethod() {
    // 执行查询操作
}

在你的示例代码中,buyBook方法被标记为只读,但是它实际上尝试执行更新操作(如bookDao.updateStock和bookDao.updateBalance),这是不允许的,因为只读事务不允许进行写操作。

③注意

如果你尝试在只读事务中执行写操作,比如更新或删除数据,数据库会抛出异常,因为这种行为违反了只读事务的规则。异常信息"java.sql.SQLException: Connection is read-only"表明你正在尝试在一个只读的数据库连接上执行写操作,这是不被允许的。

因此,如果你的方法中需要执行写操作,那么不应该将其设置为只读事务。只读属性应该仅用于那些确实不需要修改任何数据的方法。


6.5.6. 事务属性:超时

①介绍

事务超时是一个预防措施,用来确保事务不会无限期地占用数据库资源。如果在指定的时间内事务没有完成,系统会自动中断该事务,并执行回滚操作。这样做可以防止事务长时间运行,可能引起的资源占用和潜在的死锁问题,确保系统的稳定性和响应性。

②使用方式--timeout

在Spring框架中,可以通过@Transactional注解的timeout属性来设置事务的超时时间。超时时间是指事务在完成之前可以运行的最长时间。如果超过了这个时间限制,Spring将抛出一个TransactionTimedOutException异常,并且事务会被回滚。

例如,下面的代码设置了一个3秒的超时时间:

复制代码
@Transactional(timeout = 3)
public void someMethod() {
    // 事务操作
}

③观察结果

当事务超时发生时,Spring会抛出TransactionTimedOutException异常,这个异常表明事务因为超过了设定的执行时间而没有完成。这个异常的抛出是框架自动进行的,目的是中断长时间运行的事务,释放数据库资源,防止资源长时间被占用导致的问题。

复制代码
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022

这条信息告诉我们,事务因为超过了3秒的超时限制而没有完成,系统设定的最后期限是2022年6月4日16点25分39秒。此时,事务会被回滚,所有在事务中已经执行的操作都会被撤销。


6.5.7. 事务属性:回滚策略

①介绍

在Spring的声明式事务管理中,默认情况下,只有运行时异常(RuntimeException)会导致事务回滚。这意味着,如果方法内部抛出了运行时异常,Spring会自动回滚事务,撤销已经进行的所有数据库操作,以保持数据的一致性。

然而,对于非运行时异常(即预检查异常,继承自Exception类但不是RuntimeException的子类),默认情况下不会触发事务回滚

为了更精细地控制哪些异常会导致事务回滚,可以通过@Transactional注解的rollbackFor和noRollbackFor属性来设置回滚策略。rollbackFor属性用于指定当抛出这些异常时应该回滚事务,而noRollbackFor属性用于指定当抛出这些异常时不应该回滚事务。

②使用方式

可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性:需要设置一个Class类型的对象
  • rollbackForClassName属性:需要设置一个字符串类型的全类名
  • noRollbackFor属性:需要设置一个Class类型的对象
  • rollbackFor属性:需要设置一个字符串类型的全类名

使用了 @Transactional(noRollbackFor = ArithmeticException.class)注解,这意味着当 **buyBook**方法内部抛出 ArithmeticException****异常时,事务不会回滚。 这可以通过noRollbackFor属性来设置:

复制代码
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}

③观察结果

在buyBook方法中,故意通过System.out.println(1/0);制造了一个除以零的数学运算异常(ArithmeticException)。根据设置的回滚策略,即使出现了这个异常,事务也不会回滚。这可能会导致数据不一致的情况,因为方法中的其他操作(如更新图书库存和用户余额)可能已经提交到数据库。

需要注意的是,在实际应用中,通常建议让所有异常都触发事务回滚,以保持数据的完整性和一致性。设置 noRollbackFor****属性来防止事务回滚应该非常谨慎,并且只在你确定即使出现异常也应该保持数据变更的情况下使用。


6.5.8. 事务属性:隔离级别

①介绍

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

隔离级别一共有四种:

  • 读未提交:READ UNCOMMITTED
    允许Transaction01读取Transaction02未提交的修改。
  • 读已提交:READ COMMITTED、
    要求Transaction01只能读取Transaction02已提交的修改。
  • 可重复读:REPEATABLE READ
    确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
  • 串行化:SERIALIZABLE
    确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

各个隔离级别解决并发问题的能力见下表:

|------------------|--------|-----------|--------|
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |

各种数据库产品对事务隔离级别的支持程度:

|------------------|------------|-----------|
| 隔离级别 | Oracle | MySQL |
| READ UNCOMMITTED | × | √ |
| READ COMMITTED | √(默认) | √ |
| REPEATABLE READ | × | √(默认) |
| SERIALIZABLE | √ | √ |

②使用方式

复制代码
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

6.5.9. 事务属性:传播行为

①介绍

什么是事务的传播行为?

在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。

一共有七种传播行为:

常用的是:REQUIRED、REQUIRES_NEW

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**
  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
  • NEVER:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

②测试

创建接口CheckoutService

复制代码
package com.sakurapaid.spring6.tx.service;

public interface CheckoutService {
    void checkout(Integer[] bookIds, Integer userId);
}

创建实现类CheckoutServiceImpl

复制代码
package com.sakurapaid.spring6.service.impl;

@Service
public class CheckoutServiceImpl implements CheckoutService {

    @Autowired
    private BookService bookService;

    @Override
    @Transactional
    //一次购买多本图书
    public void checkout(Integer[] bookIds, Integer userId) {
        for (Integer bookId : bookIds) {
            bookService.buyBook(bookId, userId);
        }
    }
}

BookServiceImpl 添加事务注解

复制代码
package com.sakurapaid.spring6.tx.service;

import com.sakurapaid.spring6.tx.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * 图书服务实现类,提供购买图书的功能。
 */
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW) 
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    /**
     * 购买图书。
     * @param bookId 图书ID
     * @param userId 用户ID
     * 该方法会查询图书价格,更新图书库存和用户余额。
     */
    @Override
    public void buyBook(Integer bookId, Integer userId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);
        // 更新图书库存
        bookDao.updateStock(bookId);
        // 更新用户余额
        bookDao.updateBalance(userId, price);
    }
}

在BookController中添加方法:

复制代码
@Autowired
private CheckoutService checkoutService;

public void checkout(Integer[] bookIds, Integer userId){
    checkoutService.checkout(bookIds, userId);
}

在数据库中将用户的余额修改为100元

③观察结果

可以通过@Transactional中的propagation属性设置事务传播行为

修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性

**@Transactional(propagation = Propagation.REQUIRED),**默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了

**@Transactional(propagation = Propagation.REQUIRES_NEW),**表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。


6.5.10. 全注解配置事务

可以结合视频例子一起看 --> 70.事务-基于注解的声明式事务-全注解配置事务_哔哩哔哩_bilibili


  1. 添加配置类

@Configuration // 标识为配置类

@ComponentScan("com.sakurapaid.spring6") // 扫描包

@EnableTransactionManagement // 开启事务管理

复制代码
package com.sakurapaid.spring6.tx.config;

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;

/**
 * 此类作为Spring应用的核心配置类,主要负责以下功能:
 * - 使用{@link ComponentScan}注解进行包扫描,自动发现并加载指定包("com.sakurapaid.spring6")下的Spring组件。
 * - 通过{@link EnableTransactionManagement}注解启用Spring的事务管理支持。
 * - 定义并初始化关键的基础设施bean,包括数据源、JdbcTemplate以及事务管理器。
 */
@Configuration 
@ComponentScan("com.sakurapaid.spring6") 
@EnableTransactionManagement 
public class SpringConfig {

    /**
     * 创建并配置Druid数据源bean。
     * 参数说明:
     *   - 无参数输入
     * 返回值说明:
     *   - 返回一个已配置好的DruidDataSource实例,供Spring容器管理和后续数据库访问组件使用。
     * 数据源配置详情:
     *   - 使用MySQL的JDBC驱动:com.mysql.cj.jdbc.Driver
     *   - 连接本地MySQL服务器,端口3306,数据库名:spring6
     *   - 设置字符编码为UTF-8,禁用SSL连接
     *   - 用户名:root,密码:2076805863
     */
    @Bean
    public DataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring6?characterEncoding=utf-8&useSSL=false");
        dataSource.setUsername("root");
        dataSource.setPassword("2076805863");
        return dataSource;
    }

    /**
     * 创建并配置JdbcTemplate bean。
     * 参数说明:
     *   - {@code dataSource}: 已经配置好的数据源,用于提供与数据库交互的能力。
     * 返回值说明:
     *   - 返回一个关联了指定数据源的JdbcTemplate实例,该实例封装了对SQL查询和更新的操作,简化了DAO层的实现。
     */
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    /**
     * 创建并配置DataSourceTransactionManager bean。
     * 参数说明:
     *   - {@code dataSource}: 已经配置好的数据源,用于提供事务管理所需的数据库连接信息。
     * 返回值说明:
     *   - 返回一个关联了指定数据源的DataSourceTransactionManager实例,该实例实现了平台无关的事务管理逻辑,基于数据源进行事务控制。
     */
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}
  1. 测试输出

在这之前,记得在 XML 配置文件中注释相关的事务操作什么的,以免起冲突

复制代码
package com.sakurapaid.spring6.tx;

import com.sakurapaid.spring6.tx.config.SpringConfig;
import com.sakurapaid.spring6.tx.controller.BookController;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestAno {

    @Test
    public void testTxAllAnnotation(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookController accountService = applicationContext.getBean("bookController", BookController.class);
        accountService.buyBook(1, 1);
    }
}

6.6. 基于XML的声明式事务

可以结合视频例子一起看 --> 71.事务-基于XML的声明式事务-具体实现_哔哩哔哩_bilibili

6.6.1. 场景模拟

参考基于注解的声明式事务


6.6.2. 修改Spring配置文件

将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置

复制代码
<aop:config>
    <!-- 配置事务通知和切入点表达式 -->
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
</aop:config>
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- tx:method标签:配置具体的事务方法 -->
        <!-- name属性:指定方法名,可以使用星号代表多个字符 -->
        <tx:method name="get*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="find*" read-only="true"/>
    
        <!-- read-only属性:设置只读属性 -->
        <!-- rollback-for属性:设置回滚的异常 -->
        <!-- no-rollback-for属性:设置不回滚的异常 -->
        <!-- isolation属性:设置事务的隔离级别 -->
        <!-- timeout属性:设置事务的超时属性 -->
        <!-- propagation属性:设置事务的传播行为 -->
        <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
    </tx:attributes>
</tx:advice>

注意:基于xml实现的声明式事务,必须引入aspectJ的依赖

复制代码
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.2</version>
</dependency>
相关推荐
乐观的Terry10 小时前
AI翻译助手-JWT与redis无状态认证
数据库·redis·缓存
Nuanyt10 小时前
Agent 学习笔记(下篇二):Claude Code 生态、RAG 与 Memory(Stage 5–6)
笔记·python·memory·rag·aiagent·claude code·agentskill
vipxieliang10 小时前
ValidX vs 正则表达式:可读性与维护性对比
java·spring boot
胖头鱼的鱼缸(尹海文)10 小时前
胖头鱼的技术专栏-482 Oracle AI DB 23.26.4新特性一览(20261027)
数据库·人工智能·oracle
qeen8710 小时前
【Linux】磁盘读写文件原理及文件系统基本概念介绍
linux·运维·服务器·笔记·学习
无心水10 小时前
6、软件工程 核心考点与学习笔记
笔记·学习·软件工程·软考高级·架构师·软考·软考系统架构设计师
ue星空10 小时前
学完老师的课程我能不能自己也开课?
学习
3378010 小时前
5分钟搞懂登录认证和授权框架Spring Security
java·spring boot
vipxieliang10 小时前
Spring Security 6.x 实战:认证、授权与 JWT 集成
java·spring boot
Rabitebla11 小时前
【Linux 底层原理】工具篇 (三):gcc/g++ 编译工具、动静态库与编译原理
linux·运维·服务器·笔记·学习