讲讲Spring事务

讲讲 Spring 事务

Spring 事务简介

Spring 事务管理是 Spring 框架中的一项重要功能,用于简化企业级应用中的事务管理操作。它通过声明式(Declarative)或编程式(Programmatic)的方式,帮助开发者保证数据的一致性、完整性和隔离性。

事务的核心目标是确保一组操作(如数据库读写)要么全部成功,要么全部失败(回滚),从而保证系统数据的可靠性。

事务的基本概念

  1. 事务(Transaction) :事务是一组操作的集合,这些操作作为一个单元被执行。事务需要满足 ACID 特性:
    • A(原子性):事务是一个不可分割的工作单元,要么全部执行,要么全部回滚。
    • C(一致性):事务完成后,数据库必须从一个一致性状态变为另一个一致性状态。
    • I(隔离性):多个事务并发执行时,一个事务的执行不会被其他事务干扰。
    • D(持久性):事务一旦提交,其对数据库的修改就是永久的。
  2. 事务传播(Transaction Propagation):定义事务如何传播到被调用的方法。例如,如果一个方法已经启动了事务,另一个方法是否应该加入现有事务。
  3. 事务隔离级别(Transaction Isolation Level):定义多个事务并发执行时的行为,用于解决脏读、不可重复读、幻读等问题。

Spring 事务管理的方式

Spring 提供了两种事务管理方式:

  1. 声明式事务管理
    • 推荐的方式。
    • 使用 @Transactional 注解或 XML 配置事务管理。
    • 简单易用,解耦了事务管理和业务逻辑代码。
  2. 编程式事务管理
    • 通过手动调用 Spring 的事务管理 API(TransactionTemplatePlatformTransactionManager)实现。
    • 灵活但侵入性强,一般不推荐。

1、声明式事务管理

基于注解的声明式事务

@Transactional 注解是 Spring 中声明事务的核心注解,通常标注在类或方法上。

java 复制代码
@Service
public class UserService {

    @Transactional
    public void createUser(String name, int age) {
        // 插入用户数据
        userDao.insert(name, age);

        // 模拟一个异常,触发回滚
        if (age < 0) {
            throw new IllegalArgumentException("年龄不能为负数!");
        }

        // 插入日志数据
        logDao.insert("用户创建成功:" + name);
    }
}
  • 常见属性:

    • propagation:设置事务的传播行为。
    • isolation:设置事务的隔离级别。
    • timeout:设置事务的超时时间(秒)。
    • readOnly:设置事务是否为只读。
    • rollbackFor:指定哪些异常会触发事务回滚。
    • noRollbackFor:指定哪些异常不会触发事务回滚。
事务传播行为(Propagation)

事务传播行为定义当前方法事务的处理方式。常见值如下:

描述
REQUIRED 默认值。如果当前有事务存在,则加入当前事务;如果没有,则新建一个事务。
REQUIRES_NEW 总是新建一个事务。如果当前有事务存在,则暂停当前事务。
SUPPORTS 如果当前有事务存在,则加入当前事务;如果没有事务存在,则以非事务方式执行。
NOT_SUPPORTED 总是以非事务方式执行。如果当前有事务存在,则暂停当前事务。
MANDATORY 必须在一个事务中执行,如果当前没有事务存在,则抛出异常。
NEVER 总是以非事务方式执行,如果当前有事务存在,则抛出异常。
NESTED 如果当前有事务存在,则在当前事务中嵌套一个子事务(依赖于底层数据库是否支持保存点)。
事务隔离级别(Isolation)

事务隔离级别用于控制并发事务之间的相互影响。Spring 支持以下五种隔离级别:

描述
DEFAULT 默认隔离级别,使用底层数据库的默认设置。(Mysql 默认是 可重复读)
READ_UNCOMMITTED(未提交读) 允许脏读、不可重复读和幻读。性能最高,数据安全性最低。
READ_COMMITTED(提交读) 防止脏读,但可能发生不可重复读和幻读。
REPEATABLE_READ(可重复读) 防止脏读和不可重复读,但可能发生幻读。
SERIALIZABLE(串行化) 防止脏读、不可重复读和幻读。性能最低,但数据安全性最高。

各级别比较:

隔离级别 脏读 不可重复读 幻读 性能
READ UNCOMMITTED
READ COMMITTED × 较高
REPEATABLE READ × × ×
SERIALIZABLE × × ×

2、编程式事务管理

编程式事务管理需要手动控制事务的开启、提交和回滚:

java 复制代码
@Service
public class UserService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createUser(String name, int age) {
        TransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            // 插入用户数据
            userDao.insert(name, age);

            // 模拟一个异常
            if (age < 0) {
                throw new IllegalArgumentException("年龄不能为负数!");
            }

            // 插入日志数据
            logDao.insert("用户创建成功:" + name);

            // 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
            throw e;
        }
    }
}

事务回滚的规则

  1. 默认行为

    • Spring 默认会对所有未捕获的运行时异常(RuntimeExceptionError)进行事务回滚。
    • 对受检异常(CheckedException)不回滚。
  2. 自定义回滚规则

    • 使用 @TransactionalrollbackFornoRollbackFor 属性可以自定义回滚行为。
    java 复制代码
    @Transactional(rollbackFor = Exception.class)
    public void someMethod() {
        // 代码逻辑
    }
  • @Transactional 注解中如果不配置 rollbackFor 属性,那么事务只会在遇到 RuntimeException 的时候才会回滚
  • 加上 rollbackFor=Exception.class, 可以让事务在遇到非运行时异常时也会回滚。

注意事项

  1. 方法必须是由 Spring 管理的【代理对象】调用 ,否则 @Transactional 不会生效。
  2. 事务传播与嵌套事务:理解传播机制,避免事务失控。
  3. 正确处理异常:捕获异常(不抛出)可能会导致事务失效。
  4. readOnly 属性 :查询操作应设置为 readOnly = true,以优化性能。

Spring 的事务是通过代理类实现的。

什么情况下事务会失效?

导致事务失效的主要原因

1、非 public 方法

原因

  • Spring 的 @Transactional 注解只在 public 方法 上有效,因为 Spring 的事务管理基于 AOP(代理模式) 。如果 @Transactional 注解在非 public 方法上,代理类无法拦截调用,事务不会生效。

解决方法

  • 确保带有 @Transactional 注解的方法是 public

2、自身方法调用(内部方法调用)

原因

  • Spring 的事务是通过代理类实现的。如果一个方法调用同类中的另一个方法(即 内部方法调用),此时不会通过代理类,而是直接调用实际方法,导致事务失效。

示例

java 复制代码
@Service
public class TransactionService {
    @Transactional
    public void methodA() {
        methodB(); // 不会经过代理,事务失效
    }

    @Transactional
    public void methodB() {
        // 事务代码
    }
}

解决方法

  1. 将需要事务管理的方法提取到 其他类,通过类间调用确保事务代理生效。

  2. 使用 Spring 的 AopContext 手动获取代理对象调用方法:

    java 复制代码
    public void methodA() {
        ((TransactionService) AopContext.currentProxy()).methodB();
    }

3、异常未被正确传播

原因

  • Spring 默认只会回滚 运行时异常(RuntimeException错误(Error
  • 如果方法抛出了非运行时异常(如 CheckedException),Spring 不会自动回滚事务,导致事务提交。

示例

java 复制代码
@Transactional
public void updateData() throws IOException {
    throw new IOException("非运行时异常");
}

解决方法

  1. 显式配置事务的回滚规则:

    java 复制代码
    @Transactional(rollbackFor = IOException.class)
    public void updateData() throws IOException {
        // ...
    }
  2. 将非运行时异常包装为运行时异常重新抛出:

    java 复制代码
    throw new RuntimeException(e);

4、捕获异常后不抛出

原因

  • 如果异常被 try-catch 块捕获并处理(不抛出),Spring 事务管理器无法感知到异常发生,事务不会回滚。

示例

java 复制代码
@Transactional
public void updateData() {
    try {
        // 事务操作
        updateTable1();
        updateTable2(); // 抛出异常
    } catch (Exception e) {
        // 异常被捕获,事务未回滚
        System.out.println("异常被捕获:" + e.getMessage());
    }
}

解决方法

  1. 捕获异常后手动回滚:

    java 复制代码
    @Transactional
    public void updateData() {
        try {
            updateTable1();
            updateTable2();
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            throw e;
        }
    }
  2. 避免捕获异常,让 Spring 事务管理器自动处理。

5、多线程场景

原因

  • Spring 事务是基于 ThreadLocal 管理事务上下文的,事务默认只能在当前线程中生效。
  • 如果在事务方法中开启了新线程,线程上下文不再共享,事务失效。

示例

java 复制代码
@Transactional
public void updateData() {
    new Thread(() -> {
        // 此处事务无效
        updateTable();
    }).start();
}

解决方法

  1. 避免直接在事务方法中启动线程。
  2. 如果需要异步操作,可以使用 Spring 的异步事务支持(例如 @Async 和事务结合)。
  3. 手动提交事务并手动处理回滚

6、数据库引擎不支持事务

原因

  • MySQL 中只有 InnoDB 引擎支持事务。如果使用的是不支持事务的存储引擎(如 MyISAM),事务相关操作将失效。

解决方法

  • 确保数据库表的存储引擎为 InnoDB

sql 复制代码
ALTER TABLE table_name ENGINE=InnoDB;

注意事项

  • MySQL 5.5 开始,MySQL 的默认存储引擎就是 InnoDB
  • 在此之前,MySQL 的默认存储引擎是 MyISAM。因此,现代版本的 MySQL 默认支持事务,因为 InnoDB 是一个支持事务的存储引擎。
  • 即使是 MySQL 5.5+ 版本,也有可能是 MyISAM。因为可手动设置。

7、方法未被代理管理

原因

  • Spring 的事务管理基于代理。
  • 如果一个方法未被 Spring 容器管理(例如直接使用 new 创建对象实例),Spring 的事务管理器不会生效。

解决方法

  • 确保事务方法是由 Spring 容器托管的,不能直接使用 new

8、事务传播配置错误

原因

  • Spring 支持多种事务传播行为(如 REQUIREDREQUIRES_NEW 等),不同传播行为在嵌套调用时可能导致事务失效。例如:
    • 如果外部事务回滚,而嵌套事务没有独立的事务上下文,嵌套事务的操作也会被回滚。

示例

java 复制代码
@Transactional
public void methodA() {
    methodB(); // 默认传播行为为 REQUIRED,使用同一个事务
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 独立事务,方法完成后提交
}

解决方法

  • 根据业务需要,合理选择传播行为。

正确使用事务传播行为的场景

适合使用事务传播行为的场景

  • 需要分开提交或回滚的事务 :如果希望某些方法开启独立事务,与主事务隔离(如日志记录、异步任务处理等),可以使用 REQUIRES_NEW
  • 嵌套事务回滚 :如果希望某些方法是主事务的子事务,可以使用 NESTED,在子事务出错时回滚到保存点。

不适合使用事务传播行为的场景

  • 解决类内方法调用的问题:如果只是为了修复类内调用导致的事务失效问题,不建议仅依赖事务传播行为,而是应通过代理调用方法。
相关推荐
un_fired几秒前
【Spring AI】基于专属知识库的RAG智能问答小程序开发——功能优化:用户鉴权
java·人工智能·spring
martian6652 分钟前
Java并发编程从入门到实战:同步、异步、多线程核心原理全解析
java·开发语言
计算机学姐11 分钟前
基于SpringBoot的电影售票系统
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
半升酒13 分钟前
Spring MVC
java·spring
M1A117 分钟前
走进Java异步编程的世界:开启高效编程之旅
java·后端
机智的人猿泰山35 分钟前
java 线程创建Executors 和 ThreadPoolExecutor 和 CompletableFuture 三者 区别
java·开发语言
努力的搬砖人.1 小时前
Tomcat相关的面试题
java·经验分享·后端·面试·tomcat
创码小奇客1 小时前
拿捏!Java 实现关系从青铜到王者攻略
java·spring boot·spring
还是鼠鼠1 小时前
Node.js 模块加载机制--详解
java·开发语言·前端·vscode·前端框架·npm·node.js
躲在云朵里`1 小时前
Spring MVC核心技术:从请求映射到异常处理
java·spring·mvc