事务与 ACID 及失效场景

一、先明确:为什么考察事务与 ACID 及失效场景?

  1. 你是否能理解事务的核心价值(保证数据一致性),以及 ACID 四个特性的含义与关联?
  2. 能否区分声明式事务与编程式事务的适用场景,掌握 Spring 事务的基础使用?
  3. 能否识别并规避常见的事务失效场景(如异常捕获、非 public 方法、内部调用等),体现实战能力?
  4. 能否结合业务场景(如转账、下单)解释事务的作用,而非死记硬背概念?

二、先铺垫:事务的通俗认知

事务就像「银行转账操作」:从 A 账户转 1000 元到 B 账户,包含两个步骤 ------A 账户扣 1000 元、B 账户加 1000 元。

  • 正常情况:两个步骤都执行成功,转账完成;
  • 异常情况(如网络中断、系统故障):若 A 账户已扣款,但 B 账户未到账,会导致数据不一致(A 损失 1000 元,B 未收到)。事务的作用就是:保证这两个步骤 "要么都成功,要么都失败",避免数据不一致的问题

简单说:事务是「一组不可分割的数据库操作集合」,最终要么全部执行成功(提交),要么全部执行失败(回滚),核心目的是「保证数据的一致性」。

三、核心概念 1:事务的定义与分类

1. 事务的官方定义

事务(Transaction)是数据库操作的基本逻辑单位,包含一组 SQL 语句(增删改查),具备 ACID 特性,通过提交(Commit)或回滚(Rollback)保证数据的完整性和一致性。

2. 事务的两种常见分类(Spring 中高频使用)
事务类型 核心实现方式 优点 缺点 适用场景
声明式事务 基于注解(@Transactional)或 XML 配置,由 Spring 自动管理事务的开启、提交、回滚 代码简洁,无需手动编写事务逻辑,降低耦合 灵活性不足,无法精细控制事务边界 大部分常规业务场景(如简单 CRUD、下单流程)
编程式事务 基于TransactionTemplatePlatformTransactionManager,手动编写事务开启、提交、回滚逻辑 灵活性高,可精细控制事务的开启和结束 代码冗余,侵入性强,需要手动处理异常 复杂业务场景(如多条件判断是否提交事务、批量处理分批次提交)

示例:Spring 声明式事务的基础使用

复制代码
@Service
public class TransferService {
    @Autowired
    private AccountMapper accountMapper;

    // 声明式事务:标注@Transactional即可
    @Transactional(rollbackFor = Exception.class)
    public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 步骤1:A账户扣款
        accountMapper.deductBalance(fromAccount, amount);
        // 步骤2:B账户加款
        accountMapper.increaseBalance(toAccount, amount);
        // 若中间出现异常(如空指针、SQL异常),事务自动回滚,两步操作都失效
    }
}

四、核心概念 2:ACID 四大特性(事务的核心保障)

ACID 是事务的四个基本特性,缺一不可,是事务能够保证数据一致性的基础,我们结合 "银行转账" 场景逐个拆解:

ACID 特性 通俗解释(转账场景) 核心作用 失败场景(无该特性的后果)
原子性(Atomicity) 转账的 "扣款 + 加款" 两步操作是一个不可分割的整体,要么全部执行,要么全部不执行 保证事务操作的 "不可分割性",避免部分执行导致数据不一致 A 账户扣款成功,B 账户加款失败,导致数据不一致
一致性(Consistency) 转账前后,A 和 B 账户的总余额保持不变(如转账前总余额 5000 元,转账后仍为 5000 元) 保证数据从一个合法状态转换为另一个合法状态,符合业务规则 转账后总余额增加或减少,违反 "资金守恒" 的业务规则
隔离性(Isolation) 多个用户同时转账(如 C 转 D、A 转 B),各自的事务互不干扰,看不到对方事务未提交的中间状态 避免多事务并发执行时的数据干扰(如脏读、不可重复读) A 转 B 的事务未提交时,C 查询到 A 账户已扣款但 B 未到账的中间状态,导致查询结果错误
持久性(Durability) 转账事务提交后,A 账户扣款、B 账户加款的结果永久保存到数据库,即使系统崩溃也不会丢失 保证事务提交后的结果不丢失,具备持久性 事务提交后,系统突然崩溃,重启后转账记录消失,数据回滚到转账前状态
补充:ACID 特性的关联
  • 原子性是基础:保证事务操作不可分割,是一致性的前提;
  • 一致性是核心:事务的最终目的是保证数据一致;
  • 隔离性是保障:多事务并发时,通过隔离性避免干扰,间接保证一致性;
  • 持久性是结果:事务提交后,结果永久生效,是数据一致性的最终保障。

五、核心重点:事务失效的常见场景

事务失效意味着无法保证数据一致性,可能引发严重的业务问题(如资金丢失、订单状态异常)。以下是 8 种高频失效场景:

场景 1:事务方法不是public修饰(最隐蔽的失效场景)

失效原因

Spring 声明式事务基于 AOP 动态代理实现,而 AOP 默认只对public方法进行代理增强。若事务方法被private、protected或默认(default)修饰,Spring 无法识别该方法的事务注解,事务不生效。

反例(失效)
复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    // 错误:private修饰,事务失效
    @Transactional(rollbackFor = Exception.class)
    private void createOrder(Order order) {
        orderMapper.insert(order);
        // 模拟异常
        int a = 1 / 0;
    }
}
正例(生效)
复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    // 正确:public修饰,事务生效
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
        int a = 1 / 0; // 异常触发事务回滚,订单插入操作失效
    }
}

场景 2:异常被手动捕获,未抛出(最常见的失效场景)

失效原因

Spring 事务默认只有在捕获到「未处理的异常」时,才会触发回滚。若在事务方法内部手动捕获了异常(如try-catch),且未将异常抛出,Spring 无法感知异常发生,会认为事务执行成功,直接提交事务。

反例(失效)
复制代码
@Service
public class TransferService {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(String from, String to, BigDecimal amount) {
        try {
            accountMapper.deductBalance(from, amount);
            int a = 1 / 0; // 模拟异常
            accountMapper.increaseBalance(to, amount);
        } catch (Exception e) {
            // 错误:捕获异常但未抛出,Spring无法感知,事务提交
            System.out.println("转账异常:" + e.getMessage());
        }
    }
}
正例(生效)
复制代码
@Service
public class TransferService {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(String from, String to, BigDecimal amount) {
        try {
            accountMapper.deductBalance(from, amount);
            int a = 1 / 0;
            accountMapper.increaseBalance(to, amount);
        } catch (Exception e) {
            System.out.println("转账异常:" + e.getMessage());
            throw e; // 正确:捕获后抛出异常,触发事务回滚
        }
    }
}

场景 3:未指定rollbackFor,仅抛出受检异常(Exception子类非 RuntimeException)

失效原因

Spring 声明式事务默认只对「非受检异常」(RuntimeException和Error)触发回滚,对「受检异常」(如IOException、SQLException)默认不回滚。若事务方法抛出受检异常,且未通过rollbackFor指定异常类型,事务会提交,导致失效。

反例(失效)
复制代码
@Service
public class FileService {
    @Autowired
    private FileRecordMapper fileRecordMapper;

    // 错误:未指定rollbackFor,抛出IOException(受检异常),事务不回滚
    @Transactional
    public void saveFile(String filePath) throws IOException {
        fileRecordMapper.insert(new FileRecord(filePath));
        // 模拟受检异常
        throw new IOException("文件读取失败");
    }
}
正例(生效)
复制代码
@Service
public class FileService {
    @Autowired
    private FileRecordMapper fileRecordMapper;

    // 正确:指定rollbackFor = Exception.class,所有异常都触发回滚
    @Transactional(rollbackFor = Exception.class)
    public void saveFile(String filePath) throws IOException {
        fileRecordMapper.insert(new FileRecord(filePath));
        throw new IOException("文件读取失败"); // 触发事务回滚,文件记录插入失效
    }
}

场景 4:事务方法内部自我调用(非代理对象调用)

失效原因

Spring 事务基于动态代理实现,只有通过「代理对象」调用事务方法,事务才会生效。若在同一个类中,普通方法调用事务方法(内部调用),本质是「原始对象」调用,而非代理对象,Spring 无法增强事务,导致失效。

反例(失效)
复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    // 普通方法
    public void updateUserInfo(Long id, String name) {
        // 错误:内部调用事务方法,本质是原始对象调用,事务失效
        this.updateUserName(id, name);
    }

    // 事务方法
    @Transactional(rollbackFor = Exception.class)
    public void updateUserName(Long id, String name) {
        userMapper.updateNameById(id, name);
        int a = 1 / 0; // 异常不会触发回滚
    }
}
正例(生效,3 种解决方案)
复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    // 方案1:自注入代理对象
    @Autowired
    private UserService userService;

    public void updateUserInfo(Long id, String name) {
        // 正确:通过代理对象调用事务方法
        userService.updateUserName(id, name);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateUserName(Long id, String name) {
        userMapper.updateNameById(id, name);
        int a = 1 / 0; // 触发事务回滚
    }
}

// 方案2:通过ApplicationContext获取代理对象
// 方案3:将两个方法拆分到不同的类中

场景 5:数据源未配置事务管理器

失效原因

Spring 事务需要依赖PlatformTransactionManager(事务管理器)才能生效,若只添加了@Transactional注解,但未配置对应的事务管理器(如DataSourceTransactionManager),Spring 无法管理事务,导致失效。

反例(失效)
复制代码
// 仅配置数据源,未配置事务管理器,事务失效
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        // 配置数据源参数...
        return dataSource;
    }
}
正例(生效)
复制代码
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        // 配置数据源参数...
        return dataSource;
    }

    // 正确:配置事务管理器
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

场景 6:事务传播属性配置错误(如PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED)

失效原因

Spring 事务传播属性定义了 "事务方法被另一个事务方法调用时,事务的传播行为"。若配置了非事务传播属性(如PROPAGATION_SUPPORTS(无事务则不开启)、PROPAGATION_NOT_SUPPORTED(以非事务方式运行)),在无外层事务的情况下,当前方法不会开启事务,导致失效。

反例(失效)
复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    // 错误:传播属性为PROPAGATION_NOT_SUPPORTED,以非事务方式运行,事务失效
    @Transactional(propagation = Propagation.NOT_SUPPORTED, rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
        int a = 1 / 0; // 异常不会触发回滚
    }
}
正例(生效)
复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    // 正确:默认传播属性PROPAGATION_REQUIRED(需要事务,无则创建),事务生效
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
        int a = 1 / 0; // 触发事务回滚
    }
}

场景 7:数据库表不支持事务(如 MyISAM 引擎)

失效原因

事务的实现依赖数据库的支持,MySQL 中InnoDB引擎支持事务,而MyISAM引擎不支持事务(仅支持增删改查,无事务、回滚、锁机制)。若数据库表使用MyISAM引擎,即使配置了 Spring 事务,也无法生效。

反例(失效)
复制代码
-- 错误:MyISAM引擎,不支持事务
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
正例(生效)
复制代码
-- 正确:InnoDB引擎,支持事务
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

场景 8:多线程调用(事务方法开启新线程执行数据库操作)

失效原因

Spring 事务是基于「线程绑定」的,每个事务对应一个线程,线程之间的事务相互独立。若在事务方法中开启新线程,新线程中的数据库操作不会纳入当前事务管理,即使新线程抛出异常,也无法触发当前事务回滚,导致数据不一致。

反例(失效)
复制代码
@Service
public class BatchService {
    @Autowired
    private UserMapper userMapper;

    @Transactional(rollbackFor = Exception.class)
    public void batchInsertUser(List<User> userList) {
        // 主线程:插入第一个用户
        userMapper.insert(userList.get(0));

        // 新线程:插入第二个用户
        new Thread(() -> {
            userMapper.insert(userList.get(1));
            int a = 1 / 0; // 新线程异常,无法触发主线程事务回滚
        }).start();
    }
}
正例(生效:避免多线程在事务内操作,或使用分布式事务)
复制代码
@Service
public class BatchService {
    @Autowired
    private UserMapper userMapper;

    @Transactional(rollbackFor = Exception.class)
    public void batchInsertUser(List<User> userList) {
        // 正确:同一线程内执行所有数据库操作
        userMapper.insert(userList.get(0));
        userMapper.insert(userList.get(1));
        int a = 1 / 0; // 触发事务回滚,两个用户插入都失效
    }
}

六、事务失效的通用排查步骤

当遇到事务失效时,可按以下步骤逐一排查:

  1. 检查事务方法是否为public修饰;
  2. 检查是否手动捕获异常且未抛出;
  3. 检查@Transactional是否指定rollbackFor(尤其是抛出受检异常时);
  4. 检查是否存在内部方法自我调用(非代理对象调用);
  5. 检查是否配置了对应的事务管理器;
  6. 检查事务传播属性是否配置正确;
  7. 检查数据库表引擎是否为InnoDB
  8. 检查是否存在多线程调用场景。

加分项

  1. 结合项目举例 :"我在实训项目的订单模块中,最初因内部调用事务方法导致事务失效,后来通过自注入代理对象解决;还因未指定rollbackFor,抛出IOException时事务不回滚,添加rollbackFor = Exception.class后生效";
  2. 区分事务传播属性 :"我知道 Spring 事务默认传播属性是PROPAGATION_REQUIRED(无事务则创建,有事务则加入),PROPAGATION_REQUIRES_NEW是新建事务,与外层事务独立,适合需要独立事务的场景(如日志记录)";
  3. 掌握编程式事务 :"对于复杂业务场景(如分批次提交数据),我会使用TransactionTemplate实现编程式事务,灵活控制事务提交时机";
  4. 关注分布式事务:"我了解分布式场景下(多数据库 / 多服务),本地事务无法保证一致性,需要使用 Seata、TX-LCN 等分布式事务框架"。

踩坑点

  1. 误以为@Transactional注解一定生效:忽略方法修饰符、异常捕获、内部调用等场景,导致事务失效后无法排查;
  2. 未指定rollbackFor:默认仅回滚运行时异常,抛出受检异常时事务提交,导致数据不一致;
  3. 混淆数据库引擎 :使用MyISAM引擎却配置 Spring 事务,不知道表引擎不支持事务;
  4. 多线程内操作数据库:在事务方法中开启新线程执行数据库操作,不知道线程独立导致事务无法统一管理;
  5. 忽略事务管理器配置 :仅添加@Transactional注解,未配置DataSourceTransactionManager,导致事务失效。

举一反三

  1. "Spring 事务的传播属性有哪些?默认传播属性是什么?" (答案:① 常用传播属性:PROPAGATION_REQUIRED(默认)、PROPAGATION_REQUIRES_NEWPROPAGATION_SUPPORTSPROPAGATION_NOT_SUPPORTEDPROPAGATION_NEVER等;② 默认是PROPAGATION_REQUIRED:表示当前方法需要事务,若存在外层事务则加入外层事务,若不存在则新建事务);
  2. "声明式事务和编程式事务的区别是什么?各自的适用场景?"(答案:① 声明式事务:基于注解 / XML,简洁低侵入,适合常规业务;② 编程式事务:基于 API 手动控制,灵活高侵入,适合复杂业务(如分批次提交));
  3. "数据库的事务隔离级别有哪些?Spring 默认的隔离级别是什么?"(答案:① 数据库隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable);② MySQL 默认隔离级别是可重复读,Spring 默认隔离级别是数据库默认隔离级别(即 MySQL 的可重复读));
  4. "什么是脏读、不可重复读、幻读?分别对应哪个隔离级别可以解决?"(答案:① 脏读:读取到其他事务未提交的脏数据,读已提交及以上隔离级别可解决;② 不可重复读:同一事务内多次读取同一数据,结果不一致(被其他事务修改提交),可重复读及以上隔离级别可解决;③ 幻读:同一事务内多次查询同一条件的记录,条数不一致(被其他事务插入 / 删除提交),串行化隔离级别可解决);
  5. "如果项目是分布式架构(多服务多数据库),本地事务无法满足需求,你会怎么解决?"(答案:使用分布式事务框架,如 Seata(支持 AT、TCC、SAGA 模式)、TX-LCN,或基于消息队列实现最终一致性(如 RocketMQ 的事务消息))。
相关推荐
语落心生2 小时前
Flink 到 Doris 数据同步----从二阶段提交到幂等性 StreamLoader 的演进之路
数据库
程序员清风2 小时前
阿里二面:新生代垃圾回收为啥使用标记复制算法?
java·后端·面试
sino爱学习2 小时前
Java 三元表达式(?:)的常见坑总结
java·后端
❀͜͡傀儡师2 小时前
Spring Boot函数式编程:轻量级路由函数替代传统Controller
java·spring boot·后端
Mr.朱鹏2 小时前
超时订单处理方案实战指南【完整版】
java·spring boot·redis·spring·rabbitmq·rocketmq·订单
趁月色小酌***2 小时前
JAVA 知识点总结2
java·开发语言
m5655bj2 小时前
C# 在 PDF 文档中添加电子签名
开发语言·pdf·c#
虾说羊2 小时前
java中的代理详解
java
LinHenrY12272 小时前
初识C语言(预处理详解)
c语言·开发语言