程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Spring

    • Spring6 - 概述
    • Spring6 - 入门
    • Spring6 - IOC(基于XML)
    • Spring6 - IOC(基于注解)
    • spring6 - FactoryBean
    • Spring6 - Bean的作用域
    • Spring6 - Bean生命周期
    • Spring6 - Bean循环依赖
    • Spring6 - 手写IOC容器
    • Spring6 - AOP
    • Spring6 - 自定义注解
    • Spring6 - Junit
    • Spring6 - 事务
      • 1. JdbcTemplate
        • 简介
        • 准备工作
        • 实现CURD
        • 装配 JdbcTemplate
        • 测试增删改功能
        • 查询数据返回对象
        • 查询数据返回 list 集合
        • 查询返回单个的值
      • 2. 声明式事务概念
        • 事务基本概念
        • 什么是事务
        • 事务的特性
        • 编程式事务
        • 声明式事务
      • 3. 基于注解的声明式事务
        • 准备工作
        • 测试无事务情况
        • 加入事务
        • 添加事务配置
        • 添加事务注解
        • 观察结果
        • @Transactional 注解标识的位置
        • 事务属性:只读
        • 事务属性:超时
        • 事务属性:回滚策略
        • 事务属性:隔离级别
        • 事务属性:传播行为
        • 全注解配置事务
      • 4. 基于 XML 的声明式事务
        • 场景模拟
        • 修改 Spring 配置文件
    • Spring6 - Resource
    • Spring6 - 国际化
    • Spring6 - 数据校验
    • Spring6 - Cache
    • Spring集成Swagger2
  • Spring生态
  • Spring
scholar
2023-10-28
目录

Spring6 - 事务

# 1. JdbcTemplate

# 简介

image-20221217115515670

Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作。

# 准备工作

搭建子模块:Spring-jdbc-tx

加入依赖

<dependencies>
    <!--Spring jdbc  Spring 持久化层支持 jar 包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.1.6</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.15</version>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

创建 jdbc.properties

jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/Spring?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver
1
2
3
4

配置 Spring 的配置文件

Beans.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: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">

    <!-- 导入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" />

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

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

</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

准备数据库与测试表

CREATE DATABASE `Spring`;

use `Spring`;

CREATE TABLE `t_emp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1
2
3
4
5
6
7
8
9
10
11

# 实现CURD

# 装配 JdbcTemplate

创建测试类,整合 JUnit,注入 JdbcTemplate

import org.Springframework.Beans.factory.annotation.Autowired;
import org.Springframework.jdbc.core.JdbcTemplate;
import org.Springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:Beans.xml")
public class JDBCTemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    
}
1
2
3
4
5
6
7
8
9
10
11

# 测试增删改功能

@Test
// 测试增删改功能
public void testUpdate(){
    // 添加功能
	String sql = "insert into t_emp values(null,?,?,?)";
	int result = jdbcTemplate.update(sql, "张三", 23, "男");
    
    // 修改功能
	  // String sql = "update t_emp set name=? where id=?";
    // int result = jdbcTemplate.update(sql, "张三youngkbt", 1);

    // 删除功能
	  // String sql = "delete from t_emp where id=?";
	  // int result = jdbcTemplate.update(sql, 1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

image-20240427160241755

# 查询数据返回对象

public class Emp {

    private Integer id;
    private String name;
    private Integer age;
    private String sex;

    // 生成 get 和 set 方法
    //......

    @Override
    public String toString() {
        return "Emp{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 查询:返回对象
@Test
public void testSelectObject() {
    // 写法一
        // String sql = "select * from t_emp where id=?";
        // Emp empResult = jdbcTemplate.queryForObject(sql,
                // (rs, rowNum) -> {
                    // Emp emp = new Emp();
                    // emp.setId(rs.getInt("id"));
                    // emp.setName(rs.getString("name"));
                    // emp.setAge(rs.getInt("age"));
                    // emp.setSex(rs.getString("sex"));
                    // return emp;
                // }, 1);
        // System.out.println(empResult);

    // 写法二
    String sql = "select * from t_emp where id=?";
    Emp emp = jdbcTemplate.queryForObject(sql,
                  new BeanPropertyRowMapper<>(Emp.class),1);
    System.out.println(emp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

image-20240427160207306

# 查询数据返回 list 集合

@Test
// 查询多条数据为一个 list 集合
public void testSelectList(){
    String sql = "select * from t_emp";
    List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));
    System.out.println(list);
}
1
2
3
4
5
6
7

image-20240427160227489

# 查询返回单个的值

@Test
// 查询单行单列的值
public void selectCount(){
    String sql = "select count(id) from t_emp";
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    System.out.println(count);
}
1
2
3
4
5
6
7

image-20240427160302364

# 2. 声明式事务概念

# 事务基本概念

# 什么是事务

数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

# 事务的特性

A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

# 编程式事务

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

Connection conn = ...;
    
try {
    
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    
    // 核心操作
    
    // 提交事务
    conn.commit();
    
}catch(Exception e){
    
    // 回滚事务
    conn.rollBack();
    
}finally{
    
    // 释放数据库连接
    conn.close();
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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

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

# 声明式事务

既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。

封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

  • 好处 1:提高开发效率
  • 好处 2:消除了冗余的代码
  • 好处 3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化

所以,我们可以总结下面两个概念:

  • 编程式:自己写代码 实现功能
  • 声明式:通过 配置 让 框架 实现功能

# 3. 基于注解的声明式事务

# 准备工作

添加配置

在 Beans.xml 添加配置

<!--扫描组件-->
<context:component-scan base-package="com.scholar.spring6"></context:component-scan>
1
2

创建表

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,'斗破苍穹',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,'admin',50);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

创建组件 BookController:

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}
1
2
3
4
5
6
7
8
9
10

创建接口 BookService:

public interface BookService {
    void buyBook(Integer bookId, Integer userId);
}
1
2
3

创建实现类 BookServiceImpl:

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer bookId, Integer userId) {
        // 查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        // 更新图书的库存
        bookDao.updateStock(bookId);
        // 更新用户的余额
        bookDao.updateBalance(userId, price);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建接口 BookDao:

public interface BookDao {
    Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}
1
2
3
4
5
6
7

创建实现类 BookDaoImpl:

@Repository
public class BookDaoImpl implements BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
    }

    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(sql, bookId);
    }

    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance = balance - ? where user_id = ?";
        jdbcTemplate.update(sql, price, userId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 测试无事务情况

创建测试类

import org.junit.jupiter.api.Test;
import org.Springframework.Beans.factory.annotation.Autowired;
import org.Springframework.jdbc.core.JdbcTemplate;
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);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

模拟场景

用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额。

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

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

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

此时执行 sql 语句会抛出 SQLException。

image-20240427160740518

观察结果

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新。

显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败。

image-20240427160846780

# 加入事务

# 添加事务配置

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

<?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"
       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">
1
2
3
4
5
6
7
8
9
10
11

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

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 装配数据源 -->
    <property name="dataSource" ref="druidDataSource"/>
</bean>

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

# 添加事务注解

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

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

# 观察结果

由于使用了 Spring 的声明式事务,更新库存和更新余额都没有执行。

image-20240427161553533

# @Transactional 注解标识的位置

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

# 事务属性:只读

介绍

在数据库事务中,将事务设置为只读可以帮助数据库管理系统优化事务的处理。当事务被标记为只读时,它告诉数据库这个事务不会进行任何修改操作(如插入、更新或删除),只会执行查询。因此,数据库可以利用这一点来进行一些优化措施,比如减少锁的使用、提高查询效率等。

使用场景

只读事务特别适用于执行大量查询而不需要进行数据修改的场景。这可以确保事务在执行过程中不会意外地修改数据,同时还可以帮助提高性能。

使用方式

在Spring框架中,可以通过@Transactional注解来指定一个方法或类中所有方法的事务属性。如果你设置readOnly = true,就表示该事务为只读。例如:

@Transactional(readOnly = true)
public Integer getPriceByBookId(Integer bookId) {
    return bookDao.getPriceByBookId(bookId);
}
1
2
3
4

这里,getPriceByBookId方法被声明为只读,它只进行数据查询,不涉及任何数据的修改。

错误使用示例

如果你错误地将一个涉及数据修改的操作标记为只读,数据库会阻止这些修改操作并可能抛出异常。下面的例子展示了这一点:

@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
    // 查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    // 以下操作尝试修改数据,将导致异常
    bookDao.updateStock(bookId);       // 更新库存
    bookDao.updateBalance(userId, price); // 更新用户余额
}
1
2
3
4
5
6
7
8

异常

当尝试在只读事务中执行数据修改操作时,将会抛出如下异常:

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
1

这个异常表明尝试在一个声明为只读的数据库连接上执行写操作,这是不被允许的。

小结

正确使用只读事务属性可以帮助提升查询操作的性能,并保证数据的一致性。在实际应用中,确保只有那些确实不需要进行数据修改的操作被标记为只读。

# 事务属性:超时

介绍

事务超时属性用于指定一个事务允许的最大执行时间。如果事务在指定时间内未完成,则会自动回滚。这是一种重要的机制,用于防止长时间运行的事务过度占用数据库资源,例如在遇到死锁或程序错误时。设置超时时间可以帮助系统及时释放资源,避免长时间占用导致的系统性能问题。

使用场景

事务超时特别适用于那些可能会因为各种原因(如复杂查询、大量数据处理、外部系统调用等)耗时较长的操作。通过设定一个合理的超时时间,可以避免这些操作异常时影响整个系统的稳定性和响应时间。

使用方式

在Spring框架中,可以通过@Transactional注解的timeout属性来指定事务的超时时间(单位为秒)。如果事务执行时间超过这个设置值,Spring将标记事务为超时并触发回滚。

例如:

@Transactional(timeout = 3)  // 超时时间为3秒
public void processTransaction() {
    try {
        TimeUnit.SECONDS.sleep(5); // 模拟长时间处理,超过了超时设置
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 此处的操作在超时后不会执行,因为事务会被回滚
    updateDatabase();
}
1
2
3
4
5
6
7
8
9
10

观察结果

当方法执行时间超过了设置的超时时间,系统会抛出TransactionTimedOutException异常,并自动回滚事务。这样做可以防止因为某些异常情况导致的长时间事务占用问题。

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022
1

这个异常表明事务因为执行时间超出预设的限制而被系统中断和回滚。

小结

设置事务的超时属性是一种有效的资源管理策略,它帮助系统在遇到潜在的长时间运行问题时自动采取行动,确保资源的有效利用并提高系统的可用性和稳定性。在设计事务管理策略时,应当根据业务操作的复杂性和预期的执行时间合理设置超时时间。

# 事务属性:回滚策略

介绍

在Spring的事务管理中,回滚策略允许开发者精细控制哪些异常应该触发事务回滚。默认情况下,Spring事务只对运行时异常和错误回滚,而不对检查型(编译时)异常进行回滚。这种行为可以通过@Transactional注解的属性来定制,提供了灵活的异常管理策略,以适应不同的业务需求。

使用场景

  • rollbackFor: 指定哪些异常类应该触发回滚。这是用于指定特定的异常类,当这些异常发生时,即使它们是检查型异常,事务也会回滚。
  • rollbackForClassName: 与rollbackFor相同,但是你可以用异常类的字符串名称来指定,这在某些情况下可以避免类加载问题。
  • noRollbackFor: 指定哪些异常类不应触发回滚。这通常用于排除某些通常会触发回滚的运行时异常。
  • noRollbackForClassName: 与noRollbackFor相同,但使用异常类的字符串名称。

使用方式

// 设置事务在遇到ArithmeticException时不回滚
@Transactional(noRollbackFor = ArithmeticException.class)
public void processTransaction() {
    try {
        // 此处代码可能触发ArithmeticException
        int result = 10 / 0;
    } catch (ArithmeticException e) {
        System.out.println("Caught ArithmeticException, but transaction will not roll back.");
    }
    // 其他数据库操作,这些操作不会因为上面的异常而回滚
    updateDatabase();
}
1
2
3
4
5
6
7
8
9
10
11
12

观察结果

即使在processTransaction方法中发生了ArithmeticException,通常这会导致事务回滚,但由于设置了noRollbackFor属性为ArithmeticException,事务不会回滚。这意味着任何数据库操作都会被正常处理,不受异常的影响。

// 虽然发生了除零异常,但由于配置了noRollbackFor,以下更新仍会被提交
updateDatabase();
1
2

小结

通过适当配置@Transactional注解的rollbackFor和noRollbackFor属性,可以有效地控制事务的回滚行为,使其更加符合具体的业务逻辑需求。这为事务管理提供了高度的灵活性和控制力,使开发者能够根据实际场景灵活处理异常与回滚策略。

# 事务属性:隔离级别

介绍

在数据库管理系统中,事务隔离级别定义了事务在处理数据时对其他事务的可见性,即一个事务所做的更改在未提交时对其它事务的可见性。隔离级别是一个关键组件,用于避免事务过程中的各种并发问题。隔离级别从低到高可以减少并发事务带来的问题,但同时可能会降低系统的并发性能。

隔离级别一共有四种:

  • 读未提交: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)
public void defaultIsolationLevelExample() {
    // 业务逻辑
}

// 使用读未提交隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample() {
    // 业务逻辑
}

// 使用读已提交隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() {
    // 业务逻辑
}

// 使用可重复读隔离级别
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample() {
    // 业务逻辑
}

// 使用串行化隔离级别
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample() {
    // 业务逻辑
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

总结

选择正确的隔离级别需要在数据一致性和系统性能之间做出平衡。较低的隔离级别(如Read Uncommitted和Read Committed)可以提供较高的并发性,但可能导致更多的并发相关问题。较高的隔离级别(如Serializable)提供最强的数据一致性保障,但可能对系统性能产生较大影响,特别是在高负载情况下。

# 事务属性:传播行为

什么是事务的传播行为?

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

一共有七种传播行为:

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

测试

创建接口 CheckoutService:

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

创建实现类 CheckoutServiceImpl:

@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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在 BookController 中添加方法:

@Autowired
private CheckoutService checkoutService;

public void checkout(Integer[] bookIds, Integer userId){
    checkoutService.checkout(bookIds, userId);
}
1
2
3
4
5
6

在数据库中将用户的余额修改为 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() 中回滚,购买第一本图书不受影响,即能买几本就买几本。

# 全注解配置事务

添加配置类

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 javax.sql.DataSource;

@Configuration  // 标记为配置类
@ComponentScan("cn.youngkbt.Spring6")  // 指定Spring容器扫描的包路径
@EnableTransactionManagement  // 启用Spring的注解事务管理
public class SpringConfig {

    @Bean  // 声明一个Bean,方法返回的对象将被Spring容器管理
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();  // 使用Druid作为数据库连接池
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");  // 数据库驱动
        dataSource.setUrl("jdbc:mysql://localhost:3306/Spring?characterEncoding=utf8&useSSL=false");  // 数据库URL
        dataSource.setUsername("root");  // 数据库用户名
        dataSource.setPassword("root");  // 数据库密码
        return dataSource;  // 返回配置好的数据源
    }

    @Bean(name = "jdbcTemplate")  // 指定Bean的名称
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);  // 设置数据源
        return jdbcTemplate;  // 返回配置好的JdbcTemplate,用于数据库操作
    }

    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);  // 设置事务管理器的数据源
        return dataSourceTransactionManager;  // 返回配置好的事务管理器
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

测试

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

public class TxByAllAnnotationTest {

    @Test
    public void testTxAllAnnotation() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);  // 加载配置类,初始化Spring上下文
        BookController accountService = applicationContext.getBean("bookController", BookController.class);  // 从Spring上下文中获取BookController的bean
        accountService.buyBook(1, 1);  // 执行购买书籍的操作,以测试事务的配置和行为
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4. 基于 XML 的声明式事务

# 场景模拟

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

# 修改 Spring 配置文件

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

<aop:config>
    <!-- 配置事务通知和切入点表达式 -->
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* cn.youngkbt.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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

<dependency>
  <groupId>org.Springframework</groupId>
  <artifactId>Spring-aspects</artifactId>
  <version>6.0.2</version>
</dependency>
1
2
3
4
5
编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Spring6 - Junit
Spring6 - Resource

← Spring6 - Junit Spring6 - Resource→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式