为什么需要 Spring 事务管理?

数据库本身已经提供了事务机制,能够通过 COMMIT 和 ROLLBACK 等操作实现事务的提交与回滚。

但是,在 Java 项目中,如果直接使用 JDBC 管理数据库事务,开发者通常需要手动控制事务的开启、提交和回滚。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
try {
    connection.setAutoCommit(false);

    // 创建订单
    // 扣减库存
    // 扣减余额

    connection.commit();
} catch (Exception e) {
    connection.rollback();
    throw new RuntimeException(e);
}

上述代码省略了数据库连接的获取、关闭等细节。

这种方式虽然可以实现事务管理,但存在几个问题:

  1. 重复代码较多:不同业务方法都需要编写事务开启、提交、回滚等逻辑。
  2. 业务逻辑与事务管理耦合:开发者不仅需要关注具体业务,还需要处理事务的生命周期。
  3. 事务管理容易出错:如果遗漏回滚操作,或者异常处理不当,就可能导致事务行为不符合预期。

因此,Spring 在底层事务能力之上提供了一套统一的事务管理机制,帮助开发者简化事务控制。

Spring 主要支持两种事务管理方式:

  • 编程式事务:通过 TransactionTemplate 或事务管理器 API,在代码中显式控制事务。
  • 声明式事务:通常通过 @Transactional 注解声明事务,由 Spring 自动管理相应的事务逻辑。

例如,使用声明式事务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        orderMapper.insertOrder();
        stockMapper.decreaseStock();
        accountMapper.deductBalance();
    }
}

开发者只需要在业务方法上添加 @Transactional 注解,Spring 就可以在满足事务配置和代理调用条件的情况下,自动处理事务的开启、提交和回滚,而不需要在每个业务方法中重复编写事务管理代码。

Spring 声明式事务的底层主要基于 AOP 代理机制实现。 Spring 通过事务拦截器 TransactionInterceptor 拦截方法调用,再结合事务管理器 PlatformTransactionManager 完成事务控制。

需要注意的是,Spring 并没有重新实现数据库事务,而是在数据库事务的基础上提供了一层统一的管理和抽象。

总结:Spring 提供事务管理机制,主要是为了简化 Java 项目中的事务控制,减少重复代码,实现业务逻辑与事务管理逻辑的分离,提高代码的可维护性。

Spring 的编程式事务和声明式事务有什么区别?

Spring 提供了两种事务管理方式,分别是编程式事务和声明式事务。两者的目的都是对业务中的数据库操作进行事务管理,区别主要在于事务控制逻辑由谁来编写。

编程式事务要求开发者在代码中显式控制事务的执行过程,而声明式事务则允许开发者通过注解或配置声明事务规则,将具体的事务管理工作交给 Spring 完成。

1. 编程式事务

编程式事务是指开发者通过调用事务管理相关的 API,主动控制事务的执行范围。

在 Spring 中,我们可以通过 PlatformTransactionManager 直接管理事务,也可以使用 Spring 提供的 TransactionTemplate 来简化事务操作。

例如,在电商系统中,用户下单时需要创建订单并扣减库存,这两个操作必须处于同一个事务中。

使用编程式事务,可以这样实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class OrderService {

    @Autowired
    private TransactionTemplate transactionTemplate;

    public void createOrder() {
        // 检查订单参数
        checkOrder();

        // 执行事务
        transactionTemplate.execute(status -> {

            // 创建订单
            orderMapper.insertOrder();

            // 扣减库存
            stockMapper.decreaseStock();

            return null;
        });
    }
}

在上述代码中,TransactionTemplate 是 Spring 提供的事务操作模板。当程序执行到 transactionTemplate.execute() 时,Spring 会根据事务配置开启新事务或加入已有事务,然后执行回调中的业务代码。

如果业务代码正常执行完成,Spring 会根据事务状态完成相应的提交处理;如果执行过程中抛出了需要回滚的异常,Spring 则会执行相应的回滚处理。

这里需要注意,checkOrder() 并没有被写在 execute() 的回调中,因此在没有其他外围事务的情况下,它不属于这个事务范围。

由此可以看出,编程式事务的一个重要特点是:开发者能够在代码中明确指定哪些操作需要放在事务中执行。

不过,这种方式也存在一定的问题。如果系统中有大量业务方法都需要事务管理,那么开发者就需要反复编写事务模板的调用代码。虽然 Spring 已经通过 TransactionTemplate 简化了事务的开启、提交和回滚操作,但事务管理相关的代码仍然会出现在业务方法中。

为了进一步减少业务代码与事务管理代码之间的耦合,Spring 提供了声明式事务。

2. 声明式事务

声明式事务是指开发者通过注解或配置声明事务需求,而不需要在业务方法中显式编写事务管理代码。

在 Spring 中,最常见的声明式事务实现方式就是使用 @Transactional 注解。

例如,前面的下单业务可以改写为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Service
public class OrderService {

    @Transactional
    public void createOrder() {

        // 创建订单
        orderMapper.insertOrder();

        // 扣减库存
        stockMapper.decreaseStock();
    }
}

可以看到,相比于编程式事务,声明式事务不再需要开发者主动调用 TransactionTemplate。我们只需要在业务方法上添加 @Transactional 注解,就可以声明该方法需要事务管理。

那么,为什么只添加一个注解,Spring 就能够自动完成事务的开启、提交和回滚呢?

这是因为 Spring 声明式事务默认基于 AOP 代理机制实现。

当 Spring 发现某个 Bean 的方法需要事务增强时,可以为该 Bean 创建代理对象。当外部通过代理对象调用带有 @Transactional 的方法时,调用会先经过 Spring 的事务拦截器 TransactionInterceptor。

事务拦截器会根据注解中声明的事务属性,通过 PlatformTransactionManager 获取或创建事务,然后调用真正的业务方法。方法执行结束后,再根据执行结果、事务状态和回滚规则,协调事务的提交或回滚。

因此,@Transactional 并不是直接修改了方法内部的业务代码,而是借助 AOP 代理,在方法执行前后增加了事务管理逻辑。

这种方式最大的优势在于,开发者只需要关注业务逻辑,而事务管理逻辑由 Spring 统一处理,从而减少重复代码,提高代码的可读性和可维护性。

不过,由于 Spring 默认使用代理机制实现声明式事务,因此它也会受到代理调用方式的影响。例如,在同一个类中通过 this 调用另一个带有 @Transactional 的方法时,调用通常不会经过 Spring 代理,从而无法触发被调用方法自身的事务增强。

3. 两者有什么区别?

通过上面的例子可以发现,编程式事务与声明式事务并不是两种不同的数据库事务,而是 Spring 提供的两种事务管理方式。

编程式事务强调主动控制。 开发者通过事务 API 明确指定事务的执行范围,因此能够更加灵活地控制某一段业务代码是否参与事务,适用于事务边界需要精细控制的场景。

声明式事务强调声明规则。 开发者只需要通过 @Transactional 声明事务需求,由 Spring 在方法调用前后自动处理事务管理逻辑,因此更加简洁,适用于大多数常规业务场景。

需要注意的是,两种方式最终都可以通过 Spring 的事务管理器使用底层数据库提供的事务能力。它们并不是在数据库之外又创建了一套独立的事务机制。

在实际开发中,由于大多数业务只需要以某个 Service 方法作为事务边界,因此声明式事务通常更加常用。而当我们需要在一个方法内部精确划分事务范围,或者根据运行时条件动态控制事务时,编程式事务也具有一定的优势。

简单来说,编程式事务是开发者主动管理事务,而声明式事务是开发者声明事务需求,由 Spring 帮助完成管理。

@Transactional 的底层实现原理是什么?

在 Spring 中,我们只需要在业务方法上添加 @Transactional 注解,就可以让 Spring 自动管理事务,而不需要手动编写开启事务、提交事务和回滚事务的代码。

那么,为什么一个注解就能够实现这些功能呢?

@Transactional 的底层默认基于 Spring AOP 代理机制实现。 Spring 会为符合事务增强条件的 Bean 创建代理对象,当外部通过代理对象调用事务方法时,Spring 会通过事务拦截器,在业务方法执行前后加入事务管理逻辑。

需要注意,@Transactional 本身只是用于声明事务属性的注解,并不会直接开启数据库事务。真正完成事务管理的是 Spring 提供的事务拦截器和事务管理器。

1. Spring 如何识别 @Transactional 注解?

首先来看一个简单的例子:

1
2
3
4
5
6
7
8
9
@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        orderMapper.insertOrder();
        stockMapper.decreaseStock();
    }
}

当 Spring 容器启动时,如果已经启用了注解驱动的事务管理,Spring 就会注册事务管理相关的基础设施。

其中,AnnotationTransactionAttributeSource 负责读取方法或类上的 @Transactional 注解,并将注解中的配置解析为事务属性,例如事务传播行为、隔离级别、超时时间和回滚规则等。

同时,Spring 还会注册事务相关的 Advisor 和 TransactionInterceptor。

这里可以联系前面学习的 AOP 知识:

  • Pointcut(切点):判断哪些方法需要事务增强。
  • Advice(通知):由 TransactionInterceptor 提供实际的事务管理逻辑。
  • Advisor:将切点规则和事务通知组织起来,供 Spring AOP 使用。

在创建 Bean 的过程中,Spring 的自动代理创建器会判断当前 Bean 是否匹配事务相关的 Advisor。如果匹配,就可以为该 Bean 创建 AOP 代理对象。

因此,后续我们从 Spring 容器中获取到的 OrderService,可能已经不是原始的业务对象,而是能够执行事务增强逻辑的代理对象。

这个代理对象通常由 JDK 动态代理或 CGLIB 机制创建,具体选择取决于代理配置和目标类的情况。

2. 代理对象如何实现事务管理?

当外部调用 OrderService 的 createOrder() 方法时,如果调用经过 Spring 代理,就会先进入事务拦截器,而不是直接执行原始业务方法。

整个调用过程可以理解为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
调用 OrderService 代理对象
          ↓
进入 TransactionInterceptor
          ↓
读取 @Transactional 的事务属性
          ↓
通过事务管理器开启事务或加入已有事务
          ↓
执行真正的 createOrder() 方法
          ↓
根据执行结果处理事务
     ┌────┴────┐
     ↓         ↓
  正常完成   发生异常
     ↓         ↓
  根据规则   根据回滚规则
  提交事务   提交或回滚

其中,TransactionInterceptor 是 Spring 声明式事务的重要组件,它会围绕目标方法的执行完成事务管理。

为了便于理解,我们可以把它的核心逻辑简化成下面的伪代码:

 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
public Object invoke(MethodInvocation invocation) {

    // 1. 获取当前方法的事务属性
    TransactionAttribute txAttr = getTransactionAttribute();

    // 2. 根据事务属性获取或创建事务
    TransactionStatus status =
            transactionManager.getTransaction(txAttr);

    try {
        // 3. 执行真正的业务方法
        Object result = invocation.proceed();

        // 4. 正常完成后进行事务提交处理
        transactionManager.commit(status);

        return result;

    } catch (Throwable ex) {

        // 5. 根据回滚规则处理事务
        if (shouldRollback(ex, txAttr)) {
            transactionManager.rollback(status);
        } else {
            transactionManager.commit(status);
        }

        throw ex;
    }
}

需要说明的是,上面的代码只是对执行过程的简化,并不是 Spring 的真实源码。实际实现还需要处理已有事务、事务挂起与恢复、事务状态清理等情况。

通过这个过程可以发现,事务拦截器并没有修改 createOrder() 方法内部的代码,而是利用 AOP,在目标方法执行前后增加了一层事务控制逻辑。

这也说明了为什么声明式事务能够将业务逻辑和事务管理逻辑分离。

3. PlatformTransactionManager 起什么作用?

在上面的执行过程中,TransactionInterceptor 主要负责拦截方法调用,并根据事务属性决定如何处理事务。

但是,真正执行事务获取、提交和回滚操作的,是 Spring 的事务管理器。

对于普通同步数据库访问,Spring 使用 PlatformTransactionManager 作为事务管理器的核心接口,主要提供三个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface PlatformTransactionManager {

    TransactionStatus getTransaction(
        TransactionDefinition definition
    );

    void commit(TransactionStatus status);

    void rollback(TransactionStatus status);
}

其中,getTransaction() 会根据事务传播行为获取当前事务或创建新事务,commit() 和 rollback() 则负责进行相应的事务完成处理。

PlatformTransactionManager 只是一个抽象接口,Spring 会根据实际使用的数据访问技术提供不同实现。

例如,使用 JDBC 或 MyBatis 时,通常可以使用 DataSourceTransactionManager(较新版本中也可以使用 JdbcTransactionManager);使用 JPA 时,通常使用 JpaTransactionManager。

这些事务管理器最终会通过相应的数据访问技术,操作底层数据库提供的事务机制。

因此,我们可以理解为:

TransactionInterceptor 负责在什么时候管理事务,而 PlatformTransactionManager 负责具体如何管理事务。

4. Spring 如何让同一个事务中的 SQL 使用同一条数据库连接?

这里还有一个容易忽略的问题。

假设 createOrder() 方法中执行了两条 SQL,分别是创建订单和扣减库存。如果它们使用不同的数据库连接,并且各自独立提交,那么就无法简单地将它们作为同一个本地数据库事务统一回滚。

那么,Spring 是如何让这些数据库操作参与同一个事务的呢?

在常见的 JDBC 本地事务场景中,Spring 会通过 TransactionSynchronizationManager 管理与当前线程绑定的事务资源。

例如,使用 DataSourceTransactionManager 时,Spring 可以获取数据库连接,设置事务相关配置,并将对应的连接资源绑定到当前线程。

随后,业务代码通过 Spring 集成的数据访问方式获取连接时,就能够复用当前事务所关联的数据库连接。

这样,创建订单和扣减库存等 SQL 操作,就可以在同一个数据库事务中执行。

当事务完成时,Spring 再由事务管理器处理提交或回滚,并清理相关的线程绑定资源。

需要注意,这主要描述的是传统同步数据库访问中的线程绑定事务机制。对于响应式事务,Spring 使用的是另一套基于响应式上下文的事务管理方式。

5. 为什么 @Transactional 有时候会失效?

理解了 AOP 代理原理之后,就能解释一个常见问题:为什么明明添加了 @Transactional 注解,事务却没有生效?

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Service
public class OrderService {

    public void createOrder() {
        this.saveOrder();
    }

    @Transactional
    public void saveOrder() {
        orderMapper.insertOrder();
        stockMapper.decreaseStock();
    }
}

在上述代码中,外部调用 createOrder() 方法后,方法内部通过 this.saveOrder() 调用了另一个方法。

问题就在于:this.saveOrder() 是目标对象内部的方法调用,通常不会再次经过 Spring 代理对象。

由于没有经过代理,saveOrder() 方法对应的事务拦截逻辑就不会被触发,因此它上面的 @Transactional 不会因为这次内部调用而单独生效。

当然,如果外层调用已经存在事务,内部方法仍然可能在已有事务中执行,但这并不代表内部方法的事务注解被成功拦截。

除了同类方法自调用之外,异常被捕获后没有继续抛出、异常类型不符合回滚规则、方法没有经过 Spring 容器管理等,也都可能导致事务行为不符合预期。

因此,理解声明式事务时,必须记住一个前提:默认代理模式下,事务增强通常要求方法调用经过 Spring AOP 代理。

6. 总结

Spring 的 @Transactional 底层默认基于 AOP 代理机制实现。

当 Spring 启用声明式事务管理后,会通过事务属性解析器识别 @Transactional 注解,并结合事务 Advisor 和自动代理创建器,为需要事务增强的 Bean 创建代理对象。

当外部通过代理对象调用事务方法时,会进入 TransactionInterceptor。事务拦截器读取方法的事务属性(如事务传播行为、隔离级别、超时时间、是否只读以及异常回滚规则等),通过 PlatformTransactionManager 获取或创建事务,然后执行目标业务方法,最后根据执行结果和回滚规则完成相应的提交或回滚处理。

对于常见的 JDBC 本地事务,Spring 还会借助 TransactionSynchronizationManager 管理线程绑定的事务资源,使同一事务中的数据库操作能够复用相应的数据库连接。

Spring 事务的传播行为有哪些?

1. 为什么需要事务传播行为?

在实际开发中,一个业务方法往往需要调用其他业务方法,而这些方法可能都声明了事务。

例如,在电商系统中,创建订单时需要扣减商品库存:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class OrderService {

    @Autowired
    private StockService stockService;

    @Transactional
    public void createOrder() {
        // 创建订单
        orderMapper.insertOrder();

        // 扣减库存
        stockService.decreaseStock();
    }
}

@Service
public class StockService {

    @Transactional
    public void decreaseStock() {
        stockMapper.decreaseStock();
    }
}

在上述代码中,createOrder() 和 decreaseStock() 都添加了 @Transactional 注解。

当外部调用 createOrder() 时,Spring 会为其开启事务。随后,createOrder() 又调用了 decreaseStock(),此时就产生了一个问题:

既然外层方法已经存在事务,那么内层方法应该加入这个事务,还是重新开启一个独立的事务?

如果两个方法使用同一个事务,那么它们的数据库操作通常需要一起提交或回滚;如果使用两个独立的事务,则内层事务可能已经提交,而外层事务随后发生回滚。

为了解决事务方法之间的这种关系,Spring 提供了事务传播行为(Transaction Propagation)。

事务传播行为是 @Transactional 的一个重要属性,它决定了当一个事务方法调用另一个事务方法时,被调用方法应该如何处理当前已经存在的事务。

例如:

1
2
3
4
@Transactional(propagation = Propagation.REQUIRED)
public void decreaseStock() {
    stockMapper.decreaseStock();
}

其中,propagation 用于指定事务传播行为,REQUIRED 表示如果当前已经存在事务,就加入已有事务;如果当前不存在事务,就开启一个新事务。

需要注意,事务传播行为只有在 Spring 能够识别相应的事务调用时才会发挥作用。在默认的 AOP 代理模式下,同一个类内部通过 this 调用其他事务方法,通常不会触发被调用方法自身的事务增强。因此,下面的分析默认方法调用正常经过了 Spring 事务代理。

2. Spring 提供了哪七种事务传播行为?

Spring 一共提供了七种事务传播行为,可以根据当前是否存在事务,将它们理解为不同的处理策略。

传播行为 当前存在事务时 当前不存在事务时
REQUIRED 加入当前事务 创建新事务
SUPPORTS 加入当前事务 以非事务方式执行
MANDATORY 加入当前事务 抛出异常
REQUIRES_NEW 挂起当前事务,创建新事务 创建新事务
NOT_SUPPORTED 挂起当前事务,以非事务方式执行 以非事务方式执行
NEVER 抛出异常 以非事务方式执行
NESTED 在当前事务中创建嵌套事务(通常通过保存点实现) 按 REQUIRED 的方式创建新事务

其中,REQUIRED 是 Spring 默认的事务传播行为,也是实际开发中最常使用的方式。

SUPPORTS、MANDATORY、NOT_SUPPORTED 和 NEVER 的含义相对直接,主要用于规定方法是否允许在已有事务中执行。

而 REQUIRED、REQUIRES_NEW 和 NESTED 涉及不同的事务边界和回滚关系,因此需要重点理解。

3. REQUIRED:加入已有事务

REQUIRED 是 Spring 默认的传播行为。如果当前存在事务,就加入当前事务;如果不存在事务,就创建一个新事务。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Transactional
public void createOrder() {
    orderMapper.insertOrder();
    stockService.decreaseStock();
}

@Transactional(propagation = Propagation.REQUIRED)
public void decreaseStock() {
    stockMapper.decreaseStock();
}

假设外部调用 createOrder(),Spring 首先开启事务 A。

当执行到 decreaseStock() 时,由于当前已经存在事务 A,而且内层方法配置的是 REQUIRED,因此 Spring 不会再开启一个独立的物理事务,而是让 decreaseStock() 加入事务 A。

整个执行过程可以理解为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
createOrder()
    │
    ├── 开启事务 A
    │
    ├── 创建订单
    │
    ├── decreaseStock()
    │       │
    │       └── 加入事务 A
    │              │
    │              └── 扣减库存
    │
    └── 提交事务 A

由于两个方法参与同一个数据库事务,因此在正常执行的情况下,创建订单和扣减库存会一起提交。

如果内层方法出现需要回滚的异常,并将事务标记为只能回滚,那么即使外层方法捕获了异常,也可能无法正常提交事务,最终导致整体回滚。

这也是 REQUIRED 的一个重要特点:虽然外层和内层分别声明了事务,但它们可以参与同一个物理事务,最终的提交与回滚并不是相互独立的。

4. REQUIRES_NEW:创建独立的新事务

REQUIRES_NEW 表示无论当前是否存在事务,都需要创建一个新的事务。

如果当前已经存在事务,Spring 会先挂起当前事务,再开启一个新的独立事务。待新事务完成后,再恢复之前被挂起的事务。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Transactional
public void createOrder() {
    orderMapper.insertOrder();

    logService.saveLog();

    // 后续业务出现异常
    throw new RuntimeException("下单失败");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
    logMapper.insertLog();
}

假设两个方法分别位于不同的 Spring Bean 中,并且方法调用正常经过事务代理。

执行 createOrder() 时,Spring 首先开启事务 A。

当调用 saveLog() 时,由于配置了 REQUIRES_NEW,Spring 会暂时挂起事务 A,开启一个独立的事务 B 来保存日志。

如果 saveLog() 正常执行完成,事务 B 就可以先提交。随后 Spring 恢复事务 A,继续执行外层方法。

即使外层方法后来发生异常,导致事务 A 回滚,也不会自动撤销事务 B 已经提交的数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
createOrder()
    │
    ├── 开启事务 A
    │
    ├── 创建订单
    │
    ├── 挂起事务 A
    │
    ├── saveLog()
    │       │
    │       ├── 开启事务 B
    │       ├── 保存日志
    │       └── 提交事务 B
    │
    ├── 恢复事务 A
    │
    ├── 发生异常
    │
    └── 回滚事务 A

最终,订单数据被回滚,但事务 B 中已经提交的日志仍然可以保留。

因此,REQUIRES_NEW 适用于某些需要独立提交的操作,例如希望业务失败后仍然保留的操作记录。

不过,这并不意味着内层事务失败完全不会影响外层事务。如果内层方法的异常继续向外抛出,外层事务仍然可能因为这个异常而回滚。

此外,独立事务可能需要额外占用数据库连接,因此使用时还需要考虑连接池资源和事务并发情况。

REQUIRES_NEW 的核心特点是:内外层使用相互独立的物理事务,可以分别提交或回滚。

5. NESTED:在当前事务中创建嵌套事务

NESTED 表示如果当前存在事务,就在当前事务中创建一个嵌套的事务范围;如果当前不存在事务,则按照 REQUIRED 的方式开启新事务。

与 REQUIRES_NEW 不同,NESTED 通常不会创建独立的物理事务,而是依赖数据库的保存点(Savepoint)机制,在同一个物理事务内部实现局部回滚。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Transactional
public void createOrder() {
    orderMapper.insertOrder();

    try {
        stockService.decreaseStock();
    } catch (RuntimeException e) {
        // 记录库存处理失败,继续执行其他逻辑
    }
}

@Transactional(propagation = Propagation.NESTED)
public void decreaseStock() {
    stockMapper.decreaseStock();

    throw new RuntimeException("库存处理失败");
}

在支持嵌套事务的事务管理器及数据库环境下,整个过程可以理解为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
createOrder()
    │
    ├── 开启事务 A
    │
    ├── 创建订单
    │
    ├── 创建保存点 S
    │
    ├── decreaseStock()
    │       │
    │       ├── 扣减库存
    │       ├── 发生异常
    │       └── 回滚到保存点 S
    │
    ├── 外层捕获异常
    │
    └── 提交事务 A

由于内层事务回滚到了保存点,因此内层的库存修改可以被撤销,而保存点之前的订单操作仍然有机会继续提交。

但是,嵌套事务并不是独立的物理事务。

即使内层方法正常完成,只要最外层事务最终回滚,内层的修改也会一起回滚。

另外,NESTED 依赖底层事务管理器和数据库对保存点的支持,并不是在所有事务管理环境中都能够直接使用。在常见的 JDBC 本地事务场景中,DataSourceTransactionManager 可以提供相应支持。

6. REQUIRED、REQUIRES_NEW 和 NESTED 有什么区别?

这三种传播行为最容易混淆,可以重点从是否创建独立事务,以及内外层如何相互影响来理解。

对比项 REQUIRED REQUIRES_NEW NESTED
存在外层事务时 加入已有事务 创建独立新事务 创建嵌套事务范围
是否使用独立物理事务 否 是 否
是否可以局部回滚 通常不能独立回滚内层操作 可以独立回滚 可以回滚到保存点
内层已完成后,外层回滚 内层修改一起回滚 不影响内层已提交的事务 内层修改一起回滚
典型应用 多个业务操作共同提交 日志、独立处理任务 同一事务中的局部回滚

理解这三种行为时,需要特别注意:

REQUIRED 和 NESTED 都不会在已有事务之外创建独立的物理事务,但两者并不相同。REQUIRED 是参与已有事务,而 NESTED 可以通过保存点提供局部回滚能力。

REQUIRES_NEW 则会创建独立的物理事务,因此内层事务可以在外层事务完成之前独立提交。

7. 总结

Spring 事务传播行为用于解决事务方法之间相互调用时的事务处理问题,决定被调用方法应该加入已有事务、创建新事务,还是以非事务方式执行。

Spring 一共提供七种事务传播行为,其中最常用的是 REQUIRED,它也是默认配置,表示存在事务就加入,不存在事务就创建。

REQUIRES_NEW 表示始终创建一个独立的新事务,因此内外层事务可以分别提交或回滚;NESTED 则通常通过保存点机制,在同一个物理事务中实现局部回滚,但最终仍然受到外层事务提交或回滚的影响。

Spring 事务的隔离级别如何设置?

1. Spring 为什么还需要设置事务隔离级别?

在 MySQL 篇章中,我们已经介绍过事务的四种隔离级别,以及脏读、不可重复读和幻读等并发问题。

实际上,事务隔离级别是数据库提供的能力,Spring 并没有重新实现一套隔离机制。

但是,在实际开发中,不同业务可能对事务隔离级别有不同的要求。

例如,在一个电商系统中,普通订单查询可能直接使用数据库默认的隔离级别,而某些涉及并发处理的业务,可能需要根据实际情况使用其他隔离级别。

如果只能通过修改数据库的全局配置来调整隔离级别,就很难针对不同的业务方法进行灵活控制。

因此,Spring 在事务管理机制中提供了 isolation 属性,允许开发者在声明事务时指定所需的隔离级别。

例如:

1
2
3
4
5
@Transactional(isolation = Isolation.READ_COMMITTED)
public void createOrder() {
    orderMapper.insertOrder();
    stockMapper.decreaseStock();
}

通过这种方式,开发者可以在业务方法上声明事务隔离级别,而不需要在业务代码中手动编写设置数据库连接隔离级别的逻辑。

不过,需要注意的是:Spring 只是将隔离级别要求传递给底层事务管理机制,真正实现事务隔离的仍然是数据库。

2. Spring 支持哪些事务隔离级别?

Spring 通过 Isolation 枚举定义事务隔离级别,除了数据库常见的四种隔离级别外,还提供了一个 DEFAULT 选项。

Spring 隔离级别 含义
DEFAULT 使用底层数据库的默认隔离级别
READ_UNCOMMITTED 读未提交
READ_COMMITTED 读已提交
REPEATABLE_READ 可重复读
SERIALIZABLE 串行化

其中,DEFAULT 是 Spring 的默认配置。

例如:

1
2
3
4
@Transactional
public void createOrder() {
    // 业务代码
}

相当于:

1
2
3
4
@Transactional(isolation = Isolation.DEFAULT)
public void createOrder() {
    // 业务代码
}

这意味着,如果没有显式指定隔离级别,Spring 就会采用底层数据库连接的默认隔离级别。

例如,在 MySQL InnoDB 中,默认隔离级别通常是 REPEATABLE READ;而在 PostgreSQL 中,默认隔离级别通常是 READ COMMITTED。

因此,Isolation.DEFAULT 并不是 Spring 自己定义的一种数据库隔离级别,而是表示使用底层数据库的默认设置。

另外,Spring 虽然提供了这些隔离级别选项,但具体是否支持,以及实际的并发行为,仍然取决于底层数据库和事务管理器。

3. Spring 是如何设置事务隔离级别的?

前面我们已经介绍过,@Transactional 的底层主要通过 Spring AOP 和事务管理器实现。

事务隔离级别的设置也遵循相同的机制。

当外部通过代理对象调用带有 @Transactional 的方法时,TransactionInterceptor 会读取该方法的事务属性,其中就包括 isolation。

随后,事务拦截器通过 PlatformTransactionManager 获取或创建事务。

以常见的 JDBC 本地事务为例,当需要开启一个新事务时,DataSourceTransactionManager 可以根据事务属性设置数据库连接的隔离级别。

例如:

1
2
3
4
5
6
7
@Transactional(
    isolation = Isolation.READ_COMMITTED
)
public void createOrder() {
    orderMapper.insertOrder();
    stockMapper.decreaseStock();
}

其执行过程可以简化为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
调用 createOrder()
        ↓
进入 Spring AOP 代理
        ↓
TransactionInterceptor 读取事务属性
        ↓
发现 isolation = READ_COMMITTED
        ↓
通过事务管理器获取或创建事务
        ↓
如果需要开启新事务
则设置数据库连接的隔离级别
        ↓
执行 createOrder() 中的 SQL
        ↓
提交或回滚事务

对于 JDBC 事务,底层设置隔离级别的操作可以对应到:

1
2
3
connection.setTransactionIsolation(
    Connection.TRANSACTION_READ_COMMITTED
);

当然,这只是为了说明底层原理,并不意味着 Spring 一定会直接执行这一行代码。具体行为还取决于使用的事务管理器。

事务完成后,Spring 通常还会在清理连接资源时恢复之前的连接隔离级别,避免连接被复用时影响后续业务。

因此,Spring 设置事务隔离级别的本质是:读取 @Transactional 声明的隔离级别,在创建新事务时,通过底层事务管理器将隔离级别应用到相应的数据库事务中。

4. 为什么设置了隔离级别,有时候却没有生效?

这里有一个很重要的细节:

@Transactional 中声明的隔离级别,通常只有在 Spring 开启一个新的事务时才会应用。

如果当前方法只是加入一个已经存在的事务,那么它通常会沿用已有事务的隔离级别,而不是重新修改隔离级别。

例如:

 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
@Service
public class OrderService {

    @Autowired
    private StockService stockService;

    @Transactional(
        isolation = Isolation.READ_COMMITTED
    )
    public void createOrder() {
        orderMapper.insertOrder();
        stockService.decreaseStock();
    }
}

@Service
public class StockService {

    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.SERIALIZABLE
    )
    public void decreaseStock() {
        stockMapper.decreaseStock();
    }
}

在上述代码中,createOrder() 使用 READ_COMMITTED 隔离级别,而 decreaseStock() 使用 SERIALIZABLE 隔离级别。

假设外部首先调用 createOrder(),Spring 会创建事务 A,并使用 READ_COMMITTED 隔离级别。

随后,createOrder() 调用 decreaseStock()。

由于 decreaseStock() 使用的是 Propagation.REQUIRED,因此它会加入已有的事务 A,而不是重新开启事务。

此时,即使 decreaseStock() 声明了 SERIALIZABLE,也通常不会改变事务 A 已经确定的隔离级别。

也就是说,内层方法的隔离级别声明不一定能够覆盖外层事务的隔离级别。

Spring 默认通常采用宽松模式,允许内层方法加入已有事务而不强制校验隔离级别是否一致。如果希望在隔离级别不匹配时直接报错,可以在支持该功能的事务管理器中启用 validateExistingTransaction 校验。

那么,如果希望内层方法确实使用自己指定的隔离级别,该怎么办?

一种方式是使用 REQUIRES_NEW 传播行为:

1
2
3
4
5
6
7
@Transactional(
    propagation = Propagation.REQUIRES_NEW,
    isolation = Isolation.SERIALIZABLE
)
public void decreaseStock() {
    stockMapper.decreaseStock();
}

此时,如果外层已经存在事务,Spring 会挂起外层事务,为内层方法创建一个独立的新事务,并在支持的事务管理器和数据库环境中应用新事务声明的隔离级别。

这也说明,事务传播行为和事务隔离级别并不是两个完全独立的配置,它们之间存在直接联系。

传播行为决定当前方法是否创建新事务,而隔离级别则决定新事务应该使用怎样的隔离规则。

5. 总结

Spring 通过 @Transactional 的 isolation 属性设置事务隔离级别,支持 DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE 五种配置。

其中,DEFAULT 表示使用底层数据库的默认隔离级别,其余四种对应数据库常见的事务隔离级别。

Spring 本身不负责实现事务隔离,而是通过事务拦截器读取隔离级别配置,再由事务管理器在创建新事务时,将相应的隔离级别设置应用到底层数据库事务中。

需要特别注意的是,事务隔离级别通常只在创建新事务时生效。如果当前方法通过 REQUIRED 加入已有事务,就会沿用已有事务的隔离级别,而不会简单地覆盖外层事务的设置。

简单来说,数据库负责实现事务隔离,Spring 负责声明和配置隔离级别,而传播行为决定当前方法是否有机会创建使用该隔离级别的新事务。