《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 Spring Framework 的事务管理 阅读笔记 34

《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 Spring Framework 的事务管理 阅读笔记 34

12.4 Spring Framework 的事务管理

12.4.1 代码准备

为了保持 Controller-Service-Dao 的三层架构风格,需要补充一个 UserService 作为业务层的实现,为了演示方便,此处没有使用接口+实现类的方式。UserService 中提供了一个 saveAndQuery 方法,其中包含一个用户的保存动作和查询动作,在这两个动作之间插入一个引发除零异常的计算。希望当 saveAndQuery 方法执行时,保存动作之后出现除零异常,从而使保存动作回滚。

java 复制代码
package com.yangjunbo.springboot.jdbc.exampleb;

import com.yangjunbo.springboot.jdbc.examplea.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    
    public void saveAndQuery() {
        User user = new User();
        user.setName("测试保存用户");
        user.setTel("123654789");
        userDao.save(user);
        
        int i = 1 / 0;
        
        List<User> userList = userDao.findAll();
        System.out.println(userList);
    }
}

之后再创建一个新的 TransactionController,测试用的所有事务控制相关的代码和接口都放在这个 Controller 下。在 TransactionController 中注入 UserService 并调用其 saveAndQuery 方法。

java 复制代码
package com.yangjunbo.springboot.jdbc.exampleb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TransactionController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/tx1")
    public String tx1() {
        userService.saveAndQuery();
        return "success";
    }
}

测试代码准备完毕后可以先测试一下效果,重启应用后访问 /tx1 接口,浏览器中会显示除零异常的信息,但此时查询数据库的 tbl_user 表时,却发现数据库中插入了一条数据。这个运行结果与期望的不符,所以需要引入事务控制。

最后一步不是必需的,可以在 Spring Boot 的主启动类 SpringBootJdbcApplication 上标注一个开启事务控制的注解@EnableTransactionManagement,这个注解会在底层注册很多与事务控制相关的组件,当然不标注该注解也可以,因为 Spring Boot 在自动装配时已经使用了该注解。

12.4.2 编程式事务控制

Spring Framework 提供的事务控制都非常简单,下面先讲解编程式事务的使用方法。得益于 Spring Boot 的约定大于配置,不需要进行任何配置,即可得到一个能够控制事务的模板类 TransactionTemplate,使用它可以很轻松地完成事务控制。

TransactionTemplate 的使用方式相当简单,只需要在 UserService 中注入它,并且在需要控制事务的方法中调用其 executeWithoutResult 方法,并将业务逻辑代码写入传入的 Lambda 表达式中。代码提供了一个 TransactionTemplate 的使用方式,与 executeWithoutResult 方法相似的还有一个 execute 方法,使用这个方法可以得到 Lambda 表达式的返回值,不过通常情况下不会用到内部的返回值,所以更常使用的是executeWithoutResult 方法(注意 executeWithoutResult 方法来自 Spring Framework 5.2,之前较低的版本中只能使用 execute 方法)​。

java 复制代码
package com.yangjunbo.springboot.jdbc.exampleb;

import com.yangjunbo.springboot.jdbc.examplea.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.List;

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    
    public void saveAndQuery() {
        User user = new User();
        user.setName("测试保存用户");
        user.setTel("123654789");
        userDao.save(user);
        
        int i = 1 / 0;
        
        List<User> userList = userDao.findAll();
        System.out.println(userList);
    }

    @Autowired
    private TransactionTemplate transactionTemplate;

    public void saveAndQuery2() {
        transactionTemplate.executeWithoutResult(transactionStatus -> {
            User user = new User();
            user.setName("测试保存用户2");
            user.setTel("123654789");
            userDao.save(user);

            int i = 1 / 0;

            List<User> userList = userDao.findAll();
            System.out.println(userList);
        });
    }

}

接下来在 TransactionController 中编写一个 tx2 方法,并在其中调用 saveAndQuery2 方法。重新编译后访问 /tx2 接口,浏览器中仍然会响应异常信息,不过此时查看 tbl_user 表时会发现没有新的数据被插入,证明编程式事务控制已经生效。

java 复制代码
package com.yangjunbo.springboot.jdbc.exampleb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TransactionController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/tx1")
    public String tx1() {
        userService.saveAndQuery();
        return "success";
    }

    @GetMapping("/tx2")
    public String tx2() {
        userService.saveAndQuery2();
        return "success";
    }

}

12.4.3 声明式事务控制

之所以称使用 TransactionTemplate 为编程式事务,是因为每个 Service 中都需要编码注入一个 TransactionTemplate 类,并在每个需要事务控制的方法中都使用 executeWithoutResult 方法,这本身会产生大量模板代码,效率相对低一些。为此 Spring Framework 提供了第二套方案:声明式事务控制。使用声明式事务可以实现更加简单的事务控制。

1.使用 @Transactional

Spring Framework 中提供的声明式事务控制是用注解实现的,只需要在需要控制事务的方法上标注 @Transactional。代码中使用@Transactional 对 saveAndQuery3 方法进行事务控制,编写相应的 Controller 层代码后重新编译,之后访问 /tx3 接口,测试结果说明声明式事务控制也生效了。

java 复制代码
package com.yangjunbo.springboot.jdbc.exampleb;

import com.yangjunbo.springboot.jdbc.examplea.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    
    public void saveAndQuery() {
        User user = new User();
        user.setName("测试保存用户");
        user.setTel("123654789");
        userDao.save(user);
        
        int i = 1 / 0;
        
        List<User> userList = userDao.findAll();
        System.out.println(userList);
    }

    @Autowired
    private TransactionTemplate transactionTemplate;

    public void saveAndQuery2() {
        transactionTemplate.executeWithoutResult(transactionStatus -> {
            User user = new User();
            user.setName("测试保存用户2");
            user.setTel("123654789");
            userDao.save(user);

            int i = 1 / 0;

            List<User> userList = userDao.findAll();
            System.out.println(userList);
        });
    }

    @Transactional
    public void saveAndQuery3() {
        User user = new User();
        user.setName("测试保存用户");
        user.setTel("123654789");
        userDao.save(user);

        int i = 1 / 0;

        List<User> userList = userDao.findAll();
        System.out.println(userList);
    }

}
java 复制代码
package com.yangjunbo.springboot.jdbc.exampleb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TransactionController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/tx1")
    public String tx1() {
        userService.saveAndQuery();
        return "success";
    }

    @GetMapping("/tx2")
    public String tx2() {
        userService.saveAndQuery2();
        return "success";
    }

    @GetMapping("/tx3")
    public String tx3() {
        userService.saveAndQuery3();
        return "success";
    }

}

2.@Transactional 的属性

@Transactional 注解是 Spring 声明式事务管理的核心,它提供了多个属性来精细控制事务的行为。以下是该注解中常用属性的详细解析:

⚙️ @Transactional 核心属性详解

属性 类型 默认值 说明
propagation Propagation REQUIRED 事务传播行为。定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。例如,是加入当前事务,还是新建一个独立事务。
isolation Isolation DEFAULT 事务隔离级别 。定义了事务之间的数据可见性,用于解决脏读、不可重复读和幻读等并发问题。DEFAULT 表示使用底层数据库的默认隔离级别。
timeout int -1 事务超时时间 (单位:秒)。如果事务在指定时间内未完成,将自动回滚。-1 表示永不超时。
readOnly boolean false 是否为只读事务 。设置为 true 可以提示数据库和持久化框架进行性能优化(如跳过脏检查、使用更轻量级的锁)。
rollbackFor Class[] {} 触发回滚的异常类型 。默认情况下,只有 RuntimeExceptionError 会触发回滚。通过此属性可以指定其他异常(如 Exception.class)也触发回滚。
noRollbackFor Class[] {} 不触发回滚的异常类型。指定哪些异常发生时,事务依然会正常提交,而不是回滚。
transactionManager String "" 指定事务管理器。当项目中存在多个数据源和多个事务管理器时,用于指定当前事务应使用哪个事务管理器。

🔍 重点属性深入解析

1. 事务传播行为 (propagation)

这是最核心也是最复杂的属性,共有 7 种传播行为,其中最常用的是:

  • REQUIRED (默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是绝大多数业务场景的首选。
  • **REQUIRES_NEW:无论当前是否存在事务,都会创建一个新的事务,并将当前事务挂起。新事务与外层事务完全独立,互不影响。常用于记录日志、发送消息等需要独立提交的操作。
  • NESTED:如果当前存在事务,则在嵌套事务内执行(通过数据库的 Savepoint 实现);如果当前没有事务,则创建一个新事务。嵌套事务回滚不会影响外层事务,但外层事务回滚会带动嵌套事务一起回滚。
2. 事务隔离级别 (isolation)

用于控制并发事务之间的数据可见性:

  • READ_UNCOMMITTED:最低级别,允许读取未提交的数据,可能导致脏读、不可重复读和幻读。
  • READ_COMMITTED:保证只能读取已提交的数据,解决了脏读问题,但不可重复读和幻读仍可能发生。这是 Oracle、SQL Server 等数据库的默认级别。
  • REPEATABLE_READ:保证在同一事务中多次读取同一数据的结果一致,解决了脏读和不可重复读问题。这是 MySQL InnoDB 引擎的默认级别,通过 MVCC 和间隙锁在很大程度上也解决了幻读问题。
  • SERIALIZABLE:最高级别,强制事务串行执行,彻底解决所有并发问题,但性能开销最大。
3. 回滚规则 (rollbackFor / noRollbackFor)

Spring 的默认回滚策略是:仅在抛出 RuntimeException 及其子类或 Error 时回滚 。对于受检异常(Checked Exception,如 IOExceptionSQLException),默认不会回滚。

最佳实践 :在实际开发中,为了保证数据一致性,通常建议显式指定 rollbackFor = Exception.class,让所有异常都触发回滚。

java 复制代码
// 推荐写法:明确指定所有异常都触发回滚
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) throws IOException {
    // ... 业务逻辑
}
4. 只读事务 (readOnly)

对于纯粹的查询操作,强烈建议加上 readOnly = true。这不仅能向数据库优化器传递信号,提升查询性能,还能防止在只读事务中意外执行写操作。

java 复制代码
@Transactional(readOnly = true)
public List<User> listAllUsers() {
    return userRepository.findAll();
}

3.@Transactional 的作用范围

通常会将 @Transactional 标注在需要控制事务的方法上,当一个 Service 中的方法几乎都需要事务控制时,可以直接将 @Transactional 标注在整个类 / 接口上,即代表当前类的所有 public 非静态方法都会添加事务控制。支持这样做的原因是 @Transactional 的可声明范围包括 METHOD 和 TYPE。

12.4.4 事务控制失效的场景

注解声明式事务控制的本质是基于 AOP 的逻辑增强,在具体使用事务时可能会遇到事务控制失效的情况,本节列出 Spring Boot 项目中几种常见的事务控制失效场景,读者在项目开发中一定要避免。

1.方法的可访问范围不是 public

@Transactional 必须标注在 public 修饰的方法上才可以生效,如果将注解标注在非 public 方法上,则调用该方法时不会有事务控制参与。

2.接口与实现类的动态代理

如果 @Transactional 标注在 Service 层的接口上,但实现类使用 Cglib 代理(在 @EnableAspectJAutoProxy 上设置 proxyTargetClass=true)​,则事务控制会失效,这是因为基于 Cglib 的动态代理基于类增强,而忽略类上实现的接口。

3.捕获异常类型的缺失

@Transactional 默认只会捕获 RuntimeException 和 Error 的子类,如果抛出了 Exception 类型的异常,@Transactional 默认不会捕获,事务控制也会因此失效。这就是在使用 @Transactional 时都要显式声明 rollbackFor=Exception.class 的原因。

4.手动捕获异常后未抛出

如果 Service 层的方法中使用 try-catch 结构捕获了异常但没有抛出,会导致事务拦截器无法感知到异常,从而导致事务控制出现错误。请读者一定明白,事务控制就是以方法运行时抛出的异常作为裁定依据,如果 Service 层将异常捕获后"吞掉"​,Spring Framework 会认为方法执行正确,从而错误地提交本应回滚的事务。

5.调用自身方法

同一个 Service 类中,如果一个方法使用 this 调用了自身另一个带有事务控制的方法,则也会导致事务控制失效。

造成这个现象的原因是 Service 代理对象在调用自身方法时,使用 this 获取的是原始对象而不是代理对象,这就造成使用 this 调用方法时无法触发 AOP 的增强效果。

相关推荐
xzal121 小时前
Python之filter()和map()的区别(仅供学习参考)
笔记·python
Rain的Java大神之路1 小时前
高并发下的热点账户余额扣减:Redis+Lua脚本实现无锁记账
java·spring boot·redis·后端·spring cloud·缓存·lua
AI_Cloud_推荐1 小时前
SpringBoot集成百度人脸识别SDK实战:人脸检测、比对与注册
大数据·人工智能·spring boot·安全·百度·视觉检测
霸道流氓气质5 小时前
Spring AI异常处理与重试机制
java·人工智能·spring
小江的记录本11 小时前
【CSS】CSS 动画:transition、animation、transform、CSS3 新特性(附《思维导图》)
前端·css·安全·spring·前端框架·css3·动画
宸津-代码粉碎机11 小时前
AI攻防战升级!基于Spring AI构建Java应用自动免疫安全体系
java·大数据·开发语言·人工智能·python·安全·spring
LuminousCPP13 小时前
数据结构-二叉树(六):BFS层序遍历与完全二叉树判断|复用链式队列 + (N_0=N_2+1) 性质证明
c语言·数据结构·笔记·算法·二叉树·宽度优先
user_admin_god13 小时前
数据采集/交换系统设计经验
spring boot·spring·策略模式
一条破秋裤15 小时前
STM32 学习笔记:PWM 驱动 LED 呼吸灯与舵机代码
笔记·stm32·学习