本文聚焦Java后端开发中的事务知识,从基础概念到Spring实战,覆盖ACID特性、隔离级别、传播行为、失效场景、使用时机,帮助你建立完整的事务知识体系。


目录

  1. 什么是事务

  2. 事务的四大特性(ACID)

  3. 并发事务带来的问题

  4. 事务隔离级别

  5. Spring事务管理

  6. 事务传播行为

  7. @Transactional失效的常见场景

  8. 什么时候用事务,什么时候不用

  9. 事务与锁的关系

  10. 编程式事务

  11. 分布式事务简介

  12. 实战:电商下单完整示例

  13. 常见面试题精选

  14. 总结


1. 什么是事务

事务(Transaction)是数据库操作的最小工作单元,它将一组操作绑定为一个不可分割的整体。这一组操作要么全部成功,要么全部回滚,不存在中间状态。

通俗理解: 转账场景——A给B转100元,包含两步:A账户减100,B账户加100。这两步必须同时成功或同时失败,否则就会出现钱丢失或凭空产生的问题。

-- 事务示例:转账
BEGIN TRANSACTION;
​
UPDATE account SET balance = balance - 100 WHERE user_id = 'A';
UPDATE account SET balance = balance + 100 WHERE user_id = 'B';
​
-- 两步都成功才提交
COMMIT;
​
-- 如果任何一步失败,执行回滚
-- ROLLBACK;

没有事务的世界:

步骤1:A账户减100元 → 成功,余额变为900
步骤2:B账户加100元 → 失败(网络超时/数据库宕机)
结果:A少了100元,B没收到 → 100元凭空消失 ❌
​
有了事务:
步骤1:A账户减100元 → 成功
步骤2:B账户加100元 → 失败
事务回滚:A的余额恢复为1000 ✅

2. 事务的四大特性(ACID)

ACID是事务的四个基本特性的首字母缩写,是数据库事务的理论基石。

2.1 原子性(Atomicity)

事务中的操作要么全部完成,要么全部不执行,不会停留在中间状态。

类比:银行转账,"扣A的钱"和"加B的钱"是一个原子操作
      要么都发生,要么都不发生

实现原理: 数据库通过 undo log(回滚日志) 实现原子性。事务执行过程中,数据库会记录每一步操作的逆操作。如果事务失败,就按照undo log逐步回滚到事务开始前的状态。

2.2 一致性(Consistency)

事务执行前后,数据库从一个一致状态转换到另一个一致状态。数据始终满足所有约束规则(如外键约束、唯一约束、业务规则)。

类比:转账前A和B的总额是1100元,转账后总额仍然是1100元
      数据的"合法性"始终被维护
​
一致性是目的,原子性、隔离性、持久性是手段

2.3 隔离性(Isolation)

多个并发事务之间互不干扰。每个事务都感觉自己在独立操作数据库,感知不到其他事务的存在。

类比:你在ATM机取钱,旁边有人也在取钱
      你们各自看到的余额是独立的,互不影响
​
实现原理:通过锁机制 + MVCC(多版本并发控制)

2.4 持久性(Durability)

事务一旦提交,其对数据库的修改就是永久性的,即使系统崩溃也不丢失。

类比:你提交了转账,即使银行系统马上重启
      转账结果也不会丢失
​
实现原理:通过 redo log(重做日志)实现
          事务提交时先写日志,再写磁盘(WAL机制)

2.5 四个特性的关系

                    ┌─────────┐
                    │  一致性  │  ← 最终目标
                    │(Consistency)│
                    └────┬────┘
                         │
          ┌──────────────┼──────────────┐
          │              │              │
    ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴─────┐
    │  原子性    │  │  隔离性    │  │  持久性    │
    │(Atomicity) │  │(Isolation)│  │(Durability)│
    └───────────┘  └───────────┘  └───────────┘
         │              │              │
     undo log      锁 + MVCC       redo log

一致性是目的,原子性、隔离性、持久性是保证一致性的手段。


3. 并发事务带来的问题

当多个事务同时操作同一份数据时,如果没有适当的隔离控制,会出现以下问题。

3.1 脏读(Dirty Read)

一个事务读到了另一个事务尚未提交的数据。

时间线:
  事务A                          事务B
  ─────                          ─────
  开始事务                        开始事务
  读取余额 = 1000
                                 UPDATE余额 = 800(扣200,但未提交)
  读取余额 = 800  ← 脏读!
  (以为余额是800)
                                 ROLLBACK(回滚,余额恢复1000)
  基于"余额800"做了业务决策
  结果:决策基于一个根本不存在的数据 ❌

危害: 读到了最终被回滚的数据,导致业务逻辑基于错误数据做判断。

3.2 不可重复读(Non-Repeatable Read)

同一事务内,两次读取同一行数据,结果不同。原因是另一个事务在两次读取之间修改并提交了数据。

时间线:
  事务A                          事务B
  ─────                          ─────
  开始事务                        开始事务
  读取余额 = 1000
                                 UPDATE余额 = 800
                                 COMMIT(提交)
  读取余额 = 800  ← 不可重复读!
  (同一个事务内,两次读取结果不同)
  COMMIT

与脏读的区别: 脏读读到的是未提交的数据,不可重复读读到的是已提交的数据。不可重复读的问题在于同一事务内前后不一致。

3.3 幻读(Phantom Read)

同一事务内,两次执行相同的范围查询,返回的行数不同。原因是另一个事务在两次查询之间插入或删除了行。

时间线:
  事务A                          事务B
  ─────                          ─────
  开始事务                        开始事务
  SELECT COUNT(*) FROM orders
  WHERE amount > 100 → 结果:3条
                                 INSERT INTO orders(amount) VALUES(200)
                                 COMMIT
  SELECT COUNT(*) FROM orders
  WHERE amount > 100 → 结果:4条 ← 幻读!
  (凭空多出来一条记录)
  COMMIT

与不可重复读的区别: 不可重复读针对的是某一行的数据被修改,幻读针对的是行数发生变化(新增/删除)

3.4 三个问题的对比

问题 本质 涉及操作 严重程度
脏读 读到未提交的数据 读+写 最严重
不可重复读 同一行数据前后不一致 读+写(UPDATE) 中等
幻读 查询结果的行数变化 读+写(INSERT/DELETE) 较轻

4. 事务隔离级别

为了解决上述并发问题,SQL标准定义了四种隔离级别。隔离级别越高,数据越安全,但并发性能越低。

4.1 四种隔离级别

隔离级别 脏读 不可重复读 幻读 性能
读未提交(READ UNCOMMITTED) 可能 可能 可能 最高
读已提交(READ COMMITTED) 不可能 可能 可能 较高
可重复读(REPEATABLE READ) 不可能 不可能 可能 中等
串行化(SERIALIZABLE) 不可能 不可能 不可能 最低
安全性递增 →
READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE
     性能最高                                                    安全性最高
     安全性最低                                                    性能最低

4.2 各隔离级别详解

READ UNCOMMITTED(读未提交)

最低的隔离级别。一个事务可以读到其他事务尚未提交的数据。

特点:什么都不隔离,什么都能读到
问题:脏读、不可重复读、幻读都会出现
使用场景:几乎不在生产环境使用
READ COMMITTED(读已提交)

一个事务只能读到其他事务已经提交的数据。

特点:每次SELECT都生成一个新的快照
解决:脏读
残留:不可重复读、幻读
使用场景:Oracle默认级别,大多数互联网项目使用此级别
REPEATABLE READ(可重复读)

保证同一事务内多次读取同一行数据的结果一致。

特点:事务开始时创建一个快照,整个事务期间使用这个快照
解决:脏读、不可重复读
残留:理论上存在幻读(但MySQL通过MVCC+间隙锁基本解决了幻读)
使用场景:MySQL默认级别,金融、支付等对一致性要求高的场景
SERIALIZABLE(串行化)

最高的隔离级别。所有事务串行执行,完全隔离。

特点:事务排队执行,并发度最低
解决:脏读、不可重复读、幻读,全部解决
残留:无
使用场景:几乎不在生产环境使用,除非对数据一致性要求极高且并发量很低

4.3 主流数据库的默认隔离级别

数据库 默认隔离级别
MySQL REPEATABLE READ
Oracle READ COMMITTED
PostgreSQL READ COMMITTED
SQL Server READ COMMITTED

4.4 MySQL如何解决幻读

MySQL在REPEATABLE READ级别下,通过两种机制解决幻读:

1. MVCC(多版本并发控制)
   - 快照读(普通SELECT):读取的是事务开始时的快照,不会看到其他事务的修改
   - 每行数据保留多个版本,根据事务的ReadView决定读哪个版本
​
2. Next-Key Lock(临键锁 = 行锁 + 间隙锁)
   - 当前读(SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE):
     通过临键锁锁定记录及其之间的间隙,阻止其他事务在间隙中插入新记录

4.5 在Spring中设置隔离级别

@Service
public class TransferService {
​
    // 设置隔离级别为读已提交
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transfer(String from, String to, BigDecimal amount) {
        deduct(from, amount);
        add(to, amount);
    }
​
    // 设置隔离级别为可重复读
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public BigDecimal queryBalance(String userId) {
        // 整个事务期间多次查询结果一致
        BigDecimal balance1 = accountMapper.getBalance(userId);
        // ... 其他操作 ...
        BigDecimal balance2 = accountMapper.getBalance(userId);
        // balance1 == balance2
        return balance2;
    }
}

Isolation枚举的所有值:

public enum Isolation {
    DEFAULT,                // 使用数据库默认隔离级别
    READ_UNCOMMITTED,       // 读未提交
    READ_COMMITTED,         // 读已提交
    REPEATABLE_READ,        // 可重复读
    SERIALIZABLE            // 串行化
}

4.6 如何选择隔离级别

选择原则:在满足业务需求的前提下,选择尽可能低的隔离级别
​
普通业务系统(CMS、内容管理) → READ COMMITTED
电商系统(订单、库存、支付)  → READ COMMITTED 或 REPEATABLE READ
金融系统(转账、账务)        → REPEATABLE READ
对数据一致性要求极高的场景     → SERIALIZABLE(慎用,性能很差)

5. Spring事务管理

Spring提供了两种事务管理方式:声明式事务(@Transactional注解)和编程式事务(TransactionTemplate)。实际开发中以声明式事务为主。

5.1 @Transactional 注解详解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
​
    // 事务传播行为
    Propagation propagation() default Propagation.REQUIRED;
​
    // 隔离级别
    Isolation isolation() default Isolation.DEFAULT;
​
    // 超时时间(秒),超时后自动回滚
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
​
    // 是否只读事务(只读事务可以进行一些优化)
    boolean readOnly() default false;
​
    // 指定哪些异常触发回滚(默认只对RuntimeException和Error回滚)
    Class<? extends Throwable>[] rollbackFor() default {};
​
    // 指定哪些异常不触发回滚
    Class<? extends Throwable>[] noRollbackFor() default {};
}

5.2 基本使用

@Service
public class OrderService {
​
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private AccountMapper accountMapper;
​
    /**
     * 创建订单:扣库存 + 扣余额 + 插入订单
     * 任何一步失败,全部回滚
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 1. 扣减库存
        int stockRows = stockMapper.deduct(dto.getProductId(), dto.getQuantity());
        if (stockRows == 0) {
            throw new BusinessException("库存不足");
        }
​
        // 2. 扣减余额
        BigDecimal totalAmount = dto.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity()));
        int balanceRows = accountMapper.deductBalance(dto.getUserId(), totalAmount);
        if (balanceRows == 0) {
            throw new BusinessException("余额不足");
        }
​
        // 3. 创建订单
        Order order = new Order();
        order.setOrderNo(generateOrderNo());
        order.setUserId(dto.getUserId());
        order.setProductId(dto.getProductId());
        order.setQuantity(dto.getQuantity());
        order.setTotalAmount(totalAmount);
        order.setStatus(0);
        orderMapper.insert(order);
​
        // 如果上面任何一步抛出异常,整个方法的操作全部回滚
    }
​
    private String generateOrderNo() {
        return "ORD" + System.currentTimeMillis();
    }
}

5.3 rollbackFor 详解

这是最重要的知识点之一。 Spring默认只对 RuntimeExceptionError 进行回滚,受检异常(Checked Exception)不会触发回滚。

public class BusinessException extends RuntimeException {
    // 继承RuntimeException,默认会回滚 ✓
}
​
public class BusinessException extends Exception {
    // 继承Exception(受检异常),默认不会回滚 ✗
    // 需要指定 rollbackFor = Exception.class
}
@Service
public class UserService {
​
    // ❌ 默认行为:只对RuntimeException回滚
    @Transactional
    public void method1() throws IOException {
        // 如果抛出IOException(受检异常),不会回滚!
        throw new IOException("文件读取失败");
    }
​
    // ✅ 正确做法:指定rollbackFor = Exception.class
    @Transactional(rollbackFor = Exception.class)
    public void method2() throws IOException {
        // 任何Exception都会触发回滚
        throw new IOException("文件读取失败");
    }
}

最佳实践:永远写 rollbackFor = Exception.class

5.4 只读事务

@Service
public class UserService {
​
    /**
     * 只读事务:Spring会对只读事务进行优化
     * - 不会生成undo log(因为不需要回滚)
     * - 数据库可能会使用更高效的读取策略
     * - 如果尝试在只读事务中执行写操作,会抛出异常
     */
    @Transactional(readOnly = true)
    public UserVO getUserDetail(Long userId) {
        User user = userMapper.selectById(userId);
        List<Order> orders = orderMapper.selectByUserId(userId);
​
        UserVO vo = new UserVO();
        BeanUtils.copyProperties(user, vo);
        vo.setOrders(orders);
        return vo;
    }
}

5.5 超时设置

@Service
public class ReportService {
​
    /**
     * 超时事务:如果事务执行时间超过指定秒数,自动回滚
     * 用于防止长时间占用数据库连接
     */
    @Transactional(timeout = 30, rollbackFor = Exception.class)
    public ReportVO generateMonthlyReport(int year, int month) {
        // 查询大量数据并生成报表
        // 如果30秒内没完成,事务自动回滚
        List<Order> orders = orderMapper.selectByMonth(year, month);
        // ... 复杂计算 ...
        return report;
    }
}

6. 事务传播行为

传播行为定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。这是Spring事务管理中最有深度的知识点。

6.1 七种传播行为

传播行为 含义 有事务时 无事务时
REQUIRED 默认值,支持当前事务 加入当前事务 创建新事务
REQUIRES_NEW 创建独立事务 挂起当前事务,创建新事务 创建新事务
SUPPORTS 支持当前事务 加入当前事务 以非事务方式运行
NOT_SUPPORTED 不支持事务 挂起当前事务 以非事务方式运行
MANDATORY 必须有事务 加入当前事务 抛出异常
NESTED 嵌套事务 创建嵌套事务 创建新事务
NEVER 必须没有事务 抛出异常 以非事务方式运行

6.2 REQUIRED(最常用)

默认传播行为。如果当前存在事务则加入,否则创建新事务。

@Service
public class OrderService {
​
    @Autowired
    private StockService stockService;
    @Autowired
    private AccountService accountService;
​
    @Transactional(rollbackFor = Exception.class)  // 创建事务
    public void createOrder(OrderDTO dto) {
        stockService.deductStock(dto.getProductId(), dto.getQuantity());  // 加入当前事务
        accountService.deductBalance(dto.getUserId(), dto.getAmount());   // 加入当前事务
        orderMapper.insert(buildOrder(dto));  // 加入当前事务
​
        // 所有操作在同一个事务中,任何一步失败全部回滚
    }
}
​
@Service
public class StockService {
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void deductStock(Long productId, Integer quantity) {
        // 当前已有事务(由OrderService创建),加入该事务
        // 如果当前没有事务,则创建一个新事务
    }
}

特点: 多个方法共享同一个事务,同生共死。这是最常用的方式。

6.3 REQUIRES_NEW(独立事务)

总是创建新事务。如果当前有事务,则把当前事务挂起。

@Service
public class OrderService {
​
    @Autowired
    private LogService logService;
​
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        try {
            // 业务逻辑
            doCreateOrder(dto);
        } catch (Exception e) {
            // 业务失败,但日志已经记录了(因为日志在独立事务中)
            throw e;
        }
    }
​
    private void doCreateOrder(OrderDTO dto) {
        // 记录操作日志(独立事务,不受主事务影响)
        logService.recordLog("创建订单", dto.toString());
​
        // 业务操作...
        if (somethingWrong) {
            throw new BusinessException("业务失败");
            // 主事务回滚,但日志记录不会回滚 ✅
        }
    }
}
​
@Service
public class LogService {
    /**
     * REQUIRES_NEW:总是创建新事务
     * 即使主事务回滚,这里的日志记录也不会回滚
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void recordLog(String action, String detail) {
        Log log = new Log();
        log.setAction(action);
        log.setDetail(detail);
        log.setCreateTime(LocalDateTime.now());
        logMapper.insert(log);
    }
}

使用场景:

  • 操作日志记录(无论业务成功失败都要记录)

  • 调用外部接口的记录

  • 独立的子业务(失败不影响主业务)

注意事项: REQUIRES_NEW会导致外层事务挂起,如果嵌套调用过多,会占用大量数据库连接。

6.4 NESTED(嵌套事务)

如果当前有事务则创建嵌套事务(savepoint),否则创建新事务。嵌套事务可以独立回滚,不影响外层事务。

@Service
public class BatchService {
​
    @Autowired
    private ItemService itemService;
​
    /**
     * 批量处理:某一条失败不影响其他条
     */
    @Transactional(rollbackFor = Exception.class)
    public BatchResult batchProcess(List<ItemDTO> items) {
        BatchResult result = new BatchResult();
​
        for (ItemDTO item : items) {
            try {
                // 每条记录在嵌套事务中处理
                itemService.processItem(item);
                result.addSuccess(item.getId());
            } catch (Exception e) {
                // 这条失败了,但不影响其他条
                result.addFailure(item.getId(), e.getMessage());
            }
        }
​
        return result;
    }
}
​
@Service
public class ItemService {
    /**
     * NESTED:创建嵌套事务(savepoint)
     * 如果这里抛出异常,只回滚到savepoint,不影响外层事务
     */
    @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void processItem(ItemDTO item) {
        // 处理单条数据
        // 如果失败,只回滚这一条的操作
    }
}

NESTED vs REQUIRES_NEW 的区别:

对比项 NESTED REQUIRES_NEW
事务关系 嵌套在外层事务中 完全独立的新事务
回滚范围 只回滚到savepoint 整个新事务回滚
外层事务 外层事务可以捕获异常后继续 外层事务被挂起,互不影响
数据库连接 共用外层事务的连接 使用新的数据库连接
提交时机 随外层事务一起提交 独立提交

6.5 SUPPORTS 和 NOT_SUPPORTED

// SUPPORTS:有事务就加入,没有就以非事务方式运行
// 适合:查询方法,大多数时候不需要事务,但如果被事务方法调用也能正常工作
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public User getUserById(Long id) {
    return userMapper.selectById(id);
}
​
// NOT_SUPPORTED:以非事务方式运行,如果有事务就挂起
// 适合:不需要事务的耗时操作,避免长时间占用数据库连接
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public List<Data> exportData() {
    // 大量数据导出,不需要事务
    return dataMapper.selectAll();
}

6.6 MANDATORY 和 NEVER

// MANDATORY:必须在事务中调用,否则抛异常
// 适合:必须保证数据一致性的关键操作
@Transactional(propagation = Propagation.MANDATORY)
public void criticalUpdate() {
    // 如果调用方没有事务,直接抛出异常
    // 强制要求调用方开启事务
}
​
// NEVER:必须在非事务中调用,否则抛异常
// 适合:明确不能在事务中执行的操作
@Transactional(propagation = Propagation.NEVER)
public void nonTransactionalOperation() {
    // 如果调用方有事务,直接抛出异常
}

6.7 传播行为选择指南

大多数场景 → REQUIRED(默认值,不用刻意指定)
​
日志记录、审计 → REQUIRES_NEW(独立于主事务)
​
批量处理、部分失败 → NESTED(嵌套事务,可独立回滚)
​
纯查询方法 → SUPPORTS(有就加入,没有拉倒)
​
关键数据操作 → MANDATORY(强制要求调用方有事务)

7. @Transactional失效的常见场景

这是面试高频考点,也是实际开发中最容易踩的坑。

7.1 方法非public

Spring的事务是通过AOP代理实现的,AOP只能拦截public方法。

@Service
public class UserService {
​
    // ❌ private方法,事务不生效
    @Transactional(rollbackFor = Exception.class)
    private void privateMethod() {
        // 不会开启事务!
    }
​
    // ❌ protected方法,事务不生效
    @Transactional(rollbackFor = Exception.class)
    protected void protectedMethod() {
        // 不会开启事务!
    }
​
    // ✅ public方法,事务生效
    @Transactional(rollbackFor = Exception.class)
    public void publicMethod() {
        // 正常开启事务
    }
}

7.2 同类内部方法调用(最常见)

通过 this 调用同类的方法,绕过了Spring的代理对象,事务不生效。

@Service
public class UserService {
​
    // ❌ 错误:内部调用,事务不生效
    public void outerMethod() {
        // this.innerMethod() 直接调用,没有经过代理
        this.innerMethod();  // 事务不生效!
    }
​
    @Transactional(rollbackFor = Exception.class)
    public void innerMethod() {
        // 事务逻辑
    }
}

解决方案:

@Service
public class UserService {
​
    @Autowired
    private ApplicationContext applicationContext;
​
    // 方案1:注入自身代理
    @Autowired
    private UserService self;  // 注入的是代理对象
​
    // 方案2:通过ApplicationContext获取
    private UserService getSelf() {
        return applicationContext.getBean(UserService.class);
    }
​
    // 方案3:使用AopContext(需要配置exposeProxy=true)
    // @EnableAspectJAutoProxy(exposeProxy = true)
​
    public void outerMethod() {
        // ❌ this.innerMethod();  // 事务不生效
​
        // ✅ 方案1:通过代理对象调用
        self.innerMethod();
​
        // ✅ 方案2:通过ApplicationContext获取
        getSelf().innerMethod();
​
        // ✅ 方案3:通过AopContext获取
        // ((UserService) AopContext.currentProxy()).innerMethod();
    }
​
    @Transactional(rollbackFor = Exception.class)
    public void innerMethod() {
        // 事务逻辑
    }
}

7.3 异常被吞掉

catch了异常但没有重新抛出,Spring感知不到异常,不会回滚。

@Service
public class OrderService {
​
    // ❌ 错误:异常被吞掉,事务不回滚
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        try {
            orderMapper.insert(buildOrder(dto));
            stockMapper.deduct(dto.getProductId(), dto.getQuantity());
            int i = 1 / 0;  // 抛出ArithmeticException
        } catch (Exception e) {
            // 异常被catch了,Spring不知道发生了异常
            // 事务不会回滚!
            log.error("出错了", e);
        }
    }
​
    // ✅ 正确做法1:不catch,让异常自然传播
    @Transactional(rollbackFor = Exception.class)
    public void createOrderCorrect1(OrderDTO dto) {
        orderMapper.insert(buildOrder(dto));
        stockMapper.deduct(dto.getProductId(), dto.getQuantity());
        // 如果出异常,自然传播到Spring,触发回滚
    }
​
    // ✅ 正确做法2:catch后重新抛出
    @Transactional(rollbackFor = Exception.class)
    public void createOrderCorrect2(OrderDTO dto) {
        try {
            orderMapper.insert(buildOrder(dto));
            stockMapper.deduct(dto.getProductId(), dto.getQuantity());
        } catch (Exception e) {
            log.error("出错了", e);
            throw e;  // 重新抛出,让Spring感知到异常
        }
    }
​
    // ✅ 正确做法3:手动回滚
    @Transactional(rollbackFor = Exception.class)
    public void createOrderCorrect3(OrderDTO dto) {
        try {
            orderMapper.insert(buildOrder(dto));
            stockMapper.deduct(dto.getProductId(), dto.getQuantity());
        } catch (Exception e) {
            log.error("出错了", e);
            // 手动标记当前事务需要回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
}

7.4 异常类型不匹配

默认只对RuntimeException和Error回滚,受检异常不触发回滚。

@Service
public class FileService {
​
    // ❌ IOException是受检异常,默认不会触发回滚
    @Transactional
    public void importData() throws IOException {
        // 数据库操作...
        throw new IOException("文件读取失败");
        // 事务不会回滚!
    }
​
    // ✅ 指定rollbackFor = Exception.class
    @Transactional(rollbackFor = Exception.class)
    public void importDataCorrect() throws IOException {
        // 数据库操作...
        throw new IOException("文件读取失败");
        // 事务会回滚 ✅
    }
}

7.5 数据库引擎不支持事务

MySQL的MyISAM引擎不支持事务,只有InnoDB支持。

-- 查看表的引擎
SHOW TABLE STATUS WHERE Name = 'user';
​
-- 如果是MyISAM,改为InnoDB
ALTER TABLE user ENGINE = InnoDB;

7.6 异步方法或多线程

@Service
public class AsyncService {
​
    // ❌ @Async方法中事务不生效(在新线程中执行)
    @Async
    @Transactional(rollbackFor = Exception.class)
    public void asyncProcess() {
        // 这里的事务和调用方不在同一个线程
        // 异常无法传播到调用方
    }
​
    // ❌ 新线程中的操作不在事务范围内
    @Transactional(rollbackFor = Exception.class)
    public void multiThread() {
        orderMapper.insert(order);  // 在事务中 ✅
​
        new Thread(() -> {
            // 这个操作在新线程中,不在事务中
            stockMapper.deduct(productId, quantity);  // 不在事务中 ❌
        }).start();
    }
}

7.7 失效场景速查表

场景 是否生效 原因
方法非public 不生效 AOP只能代理public方法
同类内部调用 不生效 绕过了代理对象
异常被catch没抛出 生效但不回滚 Spring感知不到异常
受检异常 生效但不回滚 默认只回滚RuntimeException
MyISAM引擎 不生效 引擎不支持事务
多线程/异步 不生效 不同线程不共享事务上下文
final方法 不生效 无法被代理重写
static方法 不生效 不属于实例方法

8. 什么时候用事务,什么时候不用

8.1 必须使用事务的场景

// 场景1:多表写操作(原子性要求)
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
    accountMapper.deduct(from, amount);   // 扣款
    accountMapper.add(to, amount);        // 入账
    transferLogMapper.insert(log);        // 记录日志
    // 三步必须同时成功或同时失败
}
​
// 场景2:批量操作(要么全部成功,要么全部回滚)
@Transactional(rollbackFor = Exception.class)
public void batchImport(List<User> users) {
    for (User user : users) {
        userMapper.insert(user);
        accountMapper.initAccount(user.getId());
        roleMapper.assignDefaultRole(user.getId());
    }
    // 任何一条失败,全部回滚
}
​
// 场景3:状态流转(步骤之间有依赖)
@Transactional(rollbackFor = Exception.class)
public void payOrder(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    if (order.getStatus() != 0) {
        throw new BusinessException("订单状态异常");
    }
    order.setStatus(1);  // 待支付 → 已支付
    orderMapper.updateById(order);
    accountMapper.deduct(order.getUserId(), order.getTotalAmount());
    // 两步必须原子完成
}

8.2 不需要使用事务的场景

// 场景1:纯查询
// 不需要事务,加了反而浪费资源
public User getUser(Long id) {
    return userMapper.selectById(id);
}
​
// 场景2:单条简单写操作
// 只操作一张表的一条记录,不需要事务
public void updateNickname(Long userId, String nickname) {
    userMapper.updateNickname(userId, nickname);
}
​
// 场景3:幂等操作
// 重复执行结果一样,不需要事务保护
public void updateProfile(UserProfile profile) {
    profileMapper.updateById(profile);
}
​
// 场景4:对一致性要求极低的统计操作
public void incrementViewCount(Long articleId) {
    articleMapper.incrementViewCount(articleId);
    // 即使少计一次浏览量,影响也不大
}

8.3 场景判断速查表

场景 是否需要事务 理由
转账 必须 多表写操作,原子性要求
电商下单 必须 扣库存+扣余额+创建订单
用户注册(仅插入用户表) 不需要 单表单条操作
用户注册(初始化账户+发优惠券) 必须 多表操作
修改昵称 不需要 单表单条更新
批量导入 必须 要么全部成功,要么全部回滚
发送短信 不需要 失败可以重试补偿
查询列表 不需要 纯读操作
订单支付 必须 状态变更+扣款,原子操作

9. 事务与锁的关系

事务和锁是两个不同层面的概念,但它们紧密关联:数据库的隔离级别本质上是通过锁机制来实现的。

9.1 隔离级别的实现原理

READ UNCOMMITTED:
  读操作不加锁,直接读取数据的最新版本
  写操作加排他锁(X锁),事务结束释放
​
READ COMMITTED:
  读操作使用MVCC(每次SELECT生成新快照),不加锁
  写操作加排他锁(X锁),事务结束释放
​
REPEATABLE READ:
  读操作使用MVCC(事务开始时创建快照,全程使用)
  写操作加排他锁(X锁)+ 间隙锁(防止幻读)
​
SERIALIZABLE:
  读操作加共享锁(S锁)
  写操作加排他锁(X锁)
  所有操作串行执行

9.2 事务中的悲观锁

悲观锁必须在事务中使用,因为锁的生命周期就是事务的生命周期。

/**
 * 悲观锁必须配合事务使用
 * SELECT ... FOR UPDATE 在事务中才会生效
 */
@Service
public class StockService {
​
    @Transactional(rollbackFor = Exception.class)  // 事务必须有
    public boolean deductStock(Long productId, Integer quantity) {
        // FOR UPDATE 加的行锁,会在事务提交/回滚时自动释放
        Stock stock = stockMapper.selectForUpdate(productId);
​
        if (stock.getQuantity() < quantity) {
            throw new BusinessException("库存不足");
        }
​
        stock.setQuantity(stock.getQuantity() - quantity);
        stockMapper.updateById(stock);
        return true;
        // 事务提交时,行锁自动释放
    }
}

9.3 事务中的乐观锁

乐观锁不依赖事务,但通常也会配合事务使用。

/**
 * 乐观锁不强制要求事务
 * 但配合事务使用更安全(保证查询和更新之间数据不被修改)
 */
@Service
public class StockService {
​
    // 方案1:不用事务(简单场景可以)
    public boolean deductWithoutTx(Long productId, Integer quantity) {
        Stock stock = stockMapper.selectById(productId);
        // 在查询和更新之间,其他线程可能修改了数据
        // 但乐观锁的版本号检查会发现冲突
        int rows = stockMapper.deductWithVersion(productId, quantity, stock.getVersion());
        return rows > 0;
    }
​
    // 方案2:用事务(更安全)
    @Transactional(rollbackFor = Exception.class)
    public boolean deductWithTx(Long productId, Integer quantity) {
        Stock stock = stockMapper.selectById(productId);
        int rows = stockMapper.deductWithVersion(productId, quantity, stock.getVersion());
        // 在事务保护下,查询和更新是一个原子操作
        return rows > 0;
    }
}

9.4 总结关系

事务 ≠ 锁,但事务依赖锁来实现隔离性
​
事务是逻辑概念:一组操作的原子性保证
锁是物理机制:控制并发访问的手段
​
悲观锁:必须在事务中使用(SELECT FOR UPDATE)
乐观锁:可以不用事务,但配合事务更安全
​
事务的隔离级别 ← 通过锁/MVCC来实现

10. 编程式事务

除了声明式事务(@Transactional),Spring还提供了编程式事务管理,适合需要更精细控制事务的场景。

10.1 TransactionTemplate

@Service
public class OrderService {
​
    @Autowired
    private TransactionTemplate transactionTemplate;
​
    /**
     * 编程式事务:使用TransactionTemplate
     * 适合需要在代码中精细控制事务边界的场景
     */
    public OrderVO createOrder(OrderDTO dto) {
        return transactionTemplate.execute(status -> {
            try {
                // 扣减库存
                stockMapper.deduct(dto.getProductId(), dto.getQuantity());
​
                // 扣减余额
                accountMapper.deductBalance(dto.getUserId(), dto.getAmount());
​
                // 创建订单
                Order order = buildOrder(dto);
                orderMapper.insert(order);
​
                return OrderVO.from(order);
            } catch (Exception e) {
                // 手动标记回滚
                status.setRollbackOnly();
                throw e;
            }
        });
    }
​
    /**
     * 无返回值版本
     */
    public void updateStock(Long productId, Integer quantity) {
        transactionTemplate.executeWithoutResult(status -> {
            Stock stock = stockMapper.selectById(productId);
            if (stock.getQuantity() < quantity) {
                status.setRollbackOnly();  // 标记回滚
                return;
            }
            stock.setQuantity(stock.getQuantity() - quantity);
            stockMapper.updateById(stock);
        });
    }
}

10.2 PlatformTransactionManager

@Service
public class AdvancedTransactionService {
​
    @Autowired
    private PlatformTransactionManager transactionManager;
​
    /**
     * 使用PlatformTransactionManager手动管理事务
     * 最底层的事务管理方式,灵活性最高
     */
    public void manualTransaction() {
        // 定义事务属性
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        def.setTimeout(30);
​
        // 获取事务状态
        TransactionStatus status = transactionManager.getTransaction(def);
​
        try {
            // 业务逻辑
            doSomething();
            doSomethingElse();
​
            // 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
            throw e;
        }
    }
}

10.3 声明式 vs 编程式对比

对比项 声明式(@Transactional) 编程式(TransactionTemplate)
使用方式 注解,零侵入 代码中显式控制
灵活性 较低,整个方法是一个事务 高,可以精确控制事务边界
代码侵入 有,需要写事务管理代码
适用场景 大多数场景 需要精细控制事务的场景
可读性 一般

11. 分布式事务简介

当一个业务操作涉及多个数据库或多个微服务时,单机事务无法保证跨系统的数据一致性,需要使用分布式事务。

11.1 什么时候需要分布式事务

单机事务:操作同一个数据库的多张表
  → Spring @Transactional 就够了
​
分布式事务:操作多个数据库,或跨多个微服务
  → 需要分布式事务方案
场景示例:
  订单服务(订单库) → 扣减库存 → 扣减余额 → 创建订单
  如果订单服务、库存服务、余额服务是不同的微服务
  每个服务有自己的数据库
  → 需要分布式事务

11.2 常见的分布式事务方案

方案 原理 优点 缺点
2PC(两阶段提交) 准备阶段+提交阶段,协调者统一指挥 强一致性 性能差,协调者单点故障
TCC(Try-Confirm-Cancel) 三个阶段:预留→确认→取消 灵活,性能较好 代码侵入大,需要实现三个接口
Saga 将大事务拆分为多个小事务,失败时执行补偿操作 性能好,无锁 最终一致性,补偿逻辑复杂
本地消息表 利用本地事务+消息队列实现最终一致性 实现简单,可靠 最终一致性,有一定延迟
Seata 阿里开源的分布式事务框架,支持AT/TCC/Saga模式 开箱即用,生态好 需要额外部署协调服务

11.3 Seata AT模式示例

// 1. 引入Seata依赖
// pom.xml
/*
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>
*/
​
// 2. 在方法上添加@GlobalTransactional
@Service
public class OrderService {
​
    @Autowired
    private StockFeignClient stockFeignClient;
    @Autowired
    private AccountFeignClient accountFeignClient;
​
    /**
     * 分布式事务:跨多个微服务的事务
     * @GlobalTransactional 是Seata的全局事务注解
     */
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 调用库存服务(远程调用,不同的数据库)
        stockFeignClient.deduct(dto.getProductId(), dto.getQuantity());
​
        // 调用账户服务(远程调用,不同的数据库)
        accountFeignClient.deduct(dto.getUserId(), dto.getAmount());
​
        // 本地操作
        orderMapper.insert(buildOrder(dto));
​
        // 任何一个服务调用失败,所有服务的操作都会回滚
    }
}

12. 实战:电商下单完整示例

把前面学到的所有事务知识综合运用到一个完整的电商下单场景中。

@Service
@Slf4j
public class OrderService {
​
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private LogService logService;
​
    /**
     * 创建订单
     *
     * 涉及知识:
     * 1. @Transactional 保证原子性
     * 2. rollbackFor = Exception.class 确保任何异常都回滚
     * 3. 悲观锁(FOR UPDATE)防止库存超卖
     * 4. 传播行为:日志用REQUIRES_NEW独立记录
     */
    @Transactional(rollbackFor = Exception.class)
    public OrderVO createOrder(CreateOrderDTO dto) {
        // ============ 第一步:扣减库存(悲观锁) ============
        Stock stock = stockMapper.selectForUpdate(dto.getProductId());
        if (stock == null) {
            throw new BusinessException("商品不存在");
        }
        if (stock.getQuantity() < dto.getQuantity()) {
            throw new BusinessException("库存不足");
        }
        stock.setQuantity(stock.getQuantity() - dto.getQuantity());
        stockMapper.updateById(stock);
        log.info("库存扣减成功,商品ID: {},扣减数量: {}", dto.getProductId(), dto.getQuantity());
​
        // ============ 第二步:扣减余额 ============
        Account account = accountMapper.selectForUpdateByUserId(dto.getUserId());
        BigDecimal totalAmount = dto.getUnitPrice().multiply(BigDecimal.valueOf(dto.getQuantity()));
        if (account.getBalance().compareTo(totalAmount) < 0) {
            throw new BusinessException("余额不足");
        }
        account.setBalance(account.getBalance().subtract(totalAmount));
        accountMapper.updateById(account);
        log.info("余额扣减成功,用户ID: {},扣减金额: {}", dto.getUserId(), totalAmount);
​
        // ============ 第三步:创建订单主表 ============
        Order order = new Order();
        order.setOrderNo(generateOrderNo());
        order.setUserId(dto.getUserId());
        order.setTotalAmount(totalAmount);
        order.setStatus(OrderStatus.CREATED.getCode());
        order.setCreateTime(LocalDateTime.now());
        orderMapper.insert(order);
​
        // ============ 第四步:创建订单明细 ============
        OrderItem item = new OrderItem();
        item.setOrderId(order.getId());
        item.setProductId(dto.getProductId());
        item.setProductName(dto.getProductName());
        item.setQuantity(dto.getQuantity());
        item.setUnitPrice(dto.getUnitPrice());
        orderItemMapper.insert(item);
​
        // ============ 第五步:记录操作日志(独立事务) ============
        try {
            logService.recordOrderLog(order.getOrderNo(), "创建订单", dto.toString());
        } catch (Exception e) {
            // 日志记录失败不影响订单创建
            log.warn("订单日志记录失败: {}", e.getMessage());
        }
​
        log.info("订单创建成功,订单号: {}", order.getOrderNo());
​
        // 返回结果
        OrderVO vo = new OrderVO();
        vo.setOrderNo(order.getOrderNo());
        vo.setTotalAmount(totalAmount);
        vo.setStatus(OrderStatus.CREATED.getCode());
        return vo;
    }
​
    /**
     * 支付订单
     *
     * 涉及知识:
     * 乐观锁防止并发支付(同一个订单被重复支付)
     */
    @Transactional(rollbackFor = Exception.class)
    public void payOrder(Long orderId, Long userId) {
        Order order = orderMapper.selectById(orderId);
​
        // 校验订单归属
        if (!order.getUserId().equals(userId)) {
            throw new BusinessException("无权操作此订单");
        }
​
        // 校验订单状态(只能对待支付状态的订单进行支付)
        if (order.getStatus() != OrderStatus.CREATED.getCode()) {
            throw new BusinessException("订单状态异常,当前状态: " + order.getStatus());
        }
​
        // 乐观锁更新订单状态
        order.setStatus(OrderStatus.PAID.getCode());
        order.setPayTime(LocalDateTime.now());
        int rows = orderMapper.updateWithVersion(order);
        if (rows == 0) {
            throw new BusinessException("订单状态已变更,请刷新后重试");
        }
​
        log.info("订单支付成功,订单号: {}", order.getOrderNo());
    }
​
    /**
     * 取消订单
     */
    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(Long orderId, Long userId) {
        Order order = orderMapper.selectById(orderId);
​
        if (!order.getUserId().equals(userId)) {
            throw new BusinessException("无权操作此订单");
        }
​
        if (order.getStatus() != OrderStatus.CREATED.getCode()) {
            throw new BusinessException("只能取消待支付的订单");
        }
​
        // 回滚库存
        stockMapper.restore(order.getProductId(), order.getQuantity());
​
        // 回滚余额
        accountMapper.restoreBalance(userId, order.getTotalAmount());
​
        // 更新订单状态
        order.setStatus(OrderStatus.CANCELLED.getCode());
        order.setCancelTime(LocalDateTime.now());
        orderMapper.updateById(order);
​
        log.info("订单已取消,订单号: {}", order.getOrderNo());
    }
​
    private String generateOrderNo() {
        return "ORD" + System.currentTimeMillis() +
               String.format("%04d", new Random().nextInt(10000));
    }
}
​
/**
 * 日志服务:使用REQUIRES_NEW,独立于主事务
 */
@Service
public class LogService {
​
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void recordOrderLog(String orderNo, String action, String detail) {
        OrderLog log = new OrderLog();
        log.setOrderNo(orderNo);
        log.setAction(action);
        log.setDetail(detail);
        log.setCreateTime(LocalDateTime.now());
        orderLogMapper.insert(log);
        // 使用REQUIRES_NEW,即使主事务回滚,这条日志也会保留
    }
}

13. 常见面试题精选

Q1:@Transactional的实现原理是什么?

Spring通过AOP(面向切面编程)实现@Transactional。
​
1. Spring扫描带有@Transactional注解的类/方法
2. 为这些类创建代理对象(JDK动态代理或CGLIB代理)
3. 调用方法时,代理拦截器会:
   a. 获取数据库连接
   b. 关闭自动提交(setAutoCommit(false))
   c. 执行业务方法
   d. 如果没有异常,提交事务
   e. 如果有异常(且满足rollbackFor条件),回滚事务
   f. 释放数据库连接

Q2:事务隔离级别是数据库层面的还是Spring层面的?

事务隔离级别是数据库层面的概念,SQL标准定义了四种隔离级别。
Spring只是通过@Transactional注解的isolation参数,将隔离级别设置传递给数据库。
​
Spring本身不实现隔离级别,它只是一个"传话筒"。
实际的隔离控制由数据库(如MySQL的InnoDB引擎)通过锁和MVCC来实现。

Q3:REPEATABLE READ是如何解决幻读的?

MySQL的REPEATABLE READ级别下:
​
1. 快照读(普通SELECT):
   通过MVCC实现,读取的是事务开始时的快照
   即使其他事务插入了新记录,当前事务也看不到
​
2. 当前读(SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE):
   通过Next-Key Lock(临键锁 = 行锁 + 间隙锁)实现
   锁定记录及其之间的间隙,阻止其他事务在间隙中插入新记录

Q4:@Transactional方法A调用方法B,B上的事务会生效吗?

取决于调用方式:
​
1. 通过this调用(内部调用)→ B的事务不生效
   因为绕过了Spring代理对象
​
2. 通过代理对象调用 → B的事务生效
   例如注入自身代理或通过ApplicationContext获取
​
3. B的方法传播行为是REQUIRED → 加入A的事务
   不管怎么调用,B都在A的事务中执行

Q5:事务的ACID中,哪个最重要?

一致性(Consistency)是最终目标,其他三个是手段。
​
- 原子性保证操作要么全做要么全不做 → 保证一致性
- 隔离性保证并发事务互不干扰 → 保证一致性
- 持久性保证提交后数据不丢失 → 保证一致性
​
没有一致性,其他三个特性都没有意义。

14. 总结

一张图看懂事务

┌─────────────────────────────────────────────────────────────┐
│                      Spring事务全景图                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐   │
│  │  声明式事务   │    │  编程式事务   │    │  分布式事务   │   │
│  │ @Transactional│    │TransactionT. │    │    Seata     │   │
│  │  最常用       │    │  精细控制     │    │  跨服务/跨库  │   │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘   │
│         │                   │                   │           │
│         └───────────────────┼───────────────────┘           │
│                             │                               │
│                    ┌────────┴────────┐                      │
│                    │    ACID特性      │                      │
│                    │ 原子性/一致性/    │                      │
│                    │ 隔离性/持久性     │                      │
│                    └────────┬────────┘                      │
│                             │                               │
│              ┌──────────────┼──────────────┐                │
│              │              │              │                │
│       ┌──────┴──────┐ ┌────┴────┐ ┌───────┴───────┐       │
│       │  undo log   │ │锁+MVCC  │ │   redo log    │       │
│       │  原子性      │ │隔离性   │ │   持久性       │       │
│       └─────────────┘ └─────────┘ └───────────────┘       │
│                                                             │
│  隔离级别:READ UNCOMMITTED → READ COMMITTED                │
│           → REPEATABLE READ → SERIALIZABLE                  │
│                                                             │
│  传播行为:REQUIRED(默认) / REQUIRES_NEW / NESTED            │
│           / SUPPORTS / MANDATORY / NOT_SUPPORTED / NEVER    │
│                                                             │
│  常见失效:非public / 内部调用 / 异常被吞 / 异常类型不匹配     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

最佳实践速查

1. 永远写 rollbackFor = Exception.class
2. 事务方法必须是 public
3. 避免在事务中进行远程调用(HTTP/RPC),会延长事务持有时间
4. 避免在事务中处理大量数据,防止长事务
5. 只读查询加 readOnly = true
6. 日志记录用 REQUIRES_NEW 独立事务
7. 同类内部调用注意代理失效问题
8. 批量操作考虑用 NESTED 嵌套事务
9. 生产环境优先选择 READ COMMITTED 隔离级别
10. 涉及多服务/多库时考虑分布式事务方案

一句话总结

事务通过ACID特性保证数据的一致性,隔离级别控制并发读写的安全性与性能平衡,传播行为定义事务边界的传递规则。理解@Transactional的失效场景和底层原理,是Java后端开发的基本功。

更多推荐