1. 简介

1.1 什么是事务

在计算机科学中,事务通常指的是一系列操作,这些操作作为一个整体一起执行,要么全部成功,要么全部失败。事务是保证数据一致性的一种机制,它有四个基本特性,通常被称为ACID属性:

  • 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改要么全都执行,要么全都不执行。
  • 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。
  • 隔离性(Isolation):在并发环境中,一个事务的执行不应该被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability):一旦事务完成(即成功提交),其结果就必须能够永久保存在数据库中。

1.2 什么是Spring事务管理

Spring事务管理是Spring框架中的一个重要组成部分,它提供了一种抽象机制来管理事务。Spring事务管理可以处理编程式和声明式的两种事务管理方式。

编程式事务管理:这种方式需要在代码中明确地进行事务管理,包括开始事务、提交事务、回滚事务等。

声明式事务管理:这种方式只需要通过注解或XML配置来管理事务,无需在代码中进行事务管理。这种方式的主要优点是不需要编写大量的事务管理代码,使业务代码更加简洁、清晰。

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

1.3 @Transactional注解的作用

在Spring框架中,@Transactional是用于管理事务的关键注解。它可以应用于接口定义、类定义、公开方法上。最常见的使用场景是在服务层的方法上使用@Transactional注解来控制事务。

当在方法上使用@Transactional注解时,Spring会为这个方法创建一个代理,当这个方法被调用时,代理会首先启动一个新的事务,然后调用原始的方法。如果这个方法成功完成(即没有抛出异常),代理会提交事务;如果这个方法抛出了异常,代理会回滚事务。

通过使用@Transactional注解,我们可以很方便地进行事务控制,无需在业务代码中添加大量的事务管理代码,使代码更加简洁、清晰。

2. @Transactional注解的使用

2.1 如何在Spring中使用@Transactional

在Spring框架中,我们可以通过在方法上使用@Transactional注解来声明该方法是事务性的。当这个方法被调用时,Spring就会为这个方法创建一个事务。

例如,我们可以在一个服务类中的方法上使用@Transactional注解:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
    }
}

在上述代码中,createUser方法被@Transactional注解,所以当调用createUser方法时,Spring会为这个方法创建一个事务。

2.2 @Transactional的属性配置

@Transactional注解包含一些属性,我们可以通过设置这些属性来改变事务的行为。

  • propagation:事务的传播行为。默认值是Propagation.REQUIRED,表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • isolation:事务的隔离级别。默认值是Isolation.DEFAULT,表示使用底层数据库的默认隔离级别。
  • timeout:事务的超时时间。默认值是-1,表示永不超时。
  • readOnly:是否为只读事务。默认值是false,表示这是一个读写事务。
  • rollbackFor:需要进行回滚的异常类数组。默认值是空数组,表示所有的RuntimeException及其子类都会导致事务回滚。
  • noRollbackFor:不需要进行回滚的异常类数组。默认值是空数组。

例如,我们可以这样配置@Transactional注解:

@Transactional(propagation = Propagation.REQUIRES_NEW,
                isolation = Isolation.READ_COMMITTED,
                timeout = 3600,
                readOnly = false,
                rollbackFor = {CustomException.class},
                noRollbackFor = {AnotherCustomException.class})
public void someMethod() {
    // ...
}

在上述代码中,我们设置了@Transactional注解的各种属性,包括传播行为、隔离级别、超时时间、是否只读以及回滚规则。

3. @Transactional的工作原理

3.1 Spring如何管理事务

Spring的事务管理分为编程式事务管理和声明式事务管理两种。编程式事务管理需要在代码中显式地进行事务管理,而声明式事务管理则通过注解或XML配置来管理事务,大大简化了事务管理的复杂性。

Spring的事务管理是通过AOP(面向切面编程)来实现的。当你在一个方法上使用@Transactional注解时,Spring会在运行时为这个方法创建一个代理对象,这个代理对象会包含事务管理的代码。当你调用这个方法时,实际上是调用了代理对象的方法。

在代理对象的方法中,Spring会首先检查当前是否存在一个事务。如果不存在,Spring会创建一个新的事务。然后,Spring会执行你的方法。如果你的方法执行成功,Spring会提交事务;如果你的方法执行失败,并抛出了未被捕获的异常,Spring会回滚事务。

3.2 @Transactional的底层实现

@Transactional注解的实现主要依赖于Spring的AOP和事务管理器。

当Spring启动时,它会创建一个AnnotationTransactionAttributeSource的实例,这个实例会找到所有使用了@Transactional注解的方法,并为这些方法生成相应的事务属性定义。

然后,Spring会创建一个TransactionInterceptor的实例,这个拦截器会在运行时拦截到所有的事务方法调用,并根据事务属性定义来决定如何处理事务。

最后,Spring会创建一个ProxyFactoryBean的实例,这个实例会为所有的事务方法生成代理对象。这些代理对象会在调用事务方法时,首先调用`Transaction

4. @Transactional的传播行为

4.1 传播行为的种类

在Spring的@Transactional注解中,传播行为(propagation behavior)定义了事务的边界。Spring定义了7种传播行为:

  1. Propagation.REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
  2. Propagation.SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
  3. Propagation.MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
  4. Propagation.REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
  5. Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  6. Propagation.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. Propagation.NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与Propagation.REQUIRED类似的操作。

4.2 每种传播行为的具体作用和使用场景

  • Propagation.REQUIRED:这是最常用的传播行为。在大多数情况下,我们希望如果一个事务已经存在,那么就在这个事务中执行;否则,就创建一个新的事务。

  • Propagation.SUPPORTS:这种传播行为在调用者并不一定需要事务,但是如果有现成的事务可以加入,那么就使用它。如果没有事务,那么就按非事务方式执行。

  • Propagation.MANDATORY:这种传播行为适用于如果没有一个已经存在的事务,那么就抛出异常的情况。也就是说,这个方法必须在一个已经存在的事务中执行。

  • Propagation.REQUIRES_NEW:这种传播行为在每次都需要创建一个新的事务,并且也会将当前的事务(如果存在)挂起,直到新的事务完成。这种传播行为适用于需要在自己的新事务中执行的情况。

  • Propagation.NOT_SUPPORTED:这种传播行为适用于不需要事务的情况。如果有一个已经存在的事务,那么这个事务会被挂起,直到这个方法执行完成。

  • Propagation.NEVER:这种传播行为适用于如果存在一个事务,那么就抛出异常的情况。也就是说,这个方法必须在没有事务的情况下执行。

  • Propagation.NESTED:这种传播行为适用于需要在一个嵌套事务中执行的情况。如果当前的事务存在,那么就在这个事务的嵌套事务中执行。如果当前的事务不存在,那么就创建一个新的事务。这种传播行为需要特定的事务管理器支持。

5. @Transactional的隔离级别

事务的隔离级别是用来解决事务并发执行时可能出现的问题,如脏读、不可重复读、幻读等。不同的隔离级别提供了不同程度的隔离。在Spring的@Transactional注解中,可以通过设置隔离级别来控制事务的隔离程度。

5.1 事务的隔离级别有哪些

事务的隔离级别主要有以下四种:

  1. ISOLATION_READ_UNCOMMITTED(读未提交):这是最低的隔离级别。允许一个事务读取另一个事务未提交的数据。可能导致脏读、不可重复读和幻读。
  2. ISOLATION_READ_COMMITTED(读已提交):这个隔离级别允许一个事务读取另一个事务已提交的数据。可以避免脏读,但仍可能导致不可重复读和幻读。
  3. ISOLATION_REPEATABLE_READ(可重复读):这个隔离级别确保一个事务在整个过程中多次读取同一行数据时,每次读取的结果都是相同的。可以避免脏读和不可重复读,但仍可能导致幻读。
  4. ISOLATION_SERIALIZABLE(串行化):这是最高的隔离级别。它要求事务序列化执行,即一个事务只能在另一个事务完成后才能开始。可以避免脏读、不可重复读和幻读,但执行效率较低。

5.2 如何在@Transactional中设置隔离级别

@Transactional注解中,可以通过设置isolation属性来指定事务的隔离级别。例如:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void someMethod() {
    // 业务逻辑
}

在这个例子中,someMethod方法的事务隔离级别被设置为READ_COMMITTED。当然,你可以根据实际需求选择其他的隔离级别。需要注意的是,不同的数据库可能支持不同的隔离级别,因此在设置隔离级别时,需要考虑数据库的实际支持情况。

6. @Transactional的只读设置

在Spring的@Transactional注解中,可以通过设置readOnly属性来指定事务是否为只读。只读事务可以帮助数据库引擎优化事务,提高查询速度。

6.1 什么是只读事务

只读事务是指该事务只进行数据的读取操作,不进行任何的数据修改操作(包括:INSERT、UPDATE、DELETE等)。在某些场景下,我们知道事务只需要读取数据,不需要修改数据,这时,我们可以将事务设置为只读,以帮助数据库做一些优化,比如避免一些不必要的锁操作,从而提高查询效率。

6.2 如何在@Transactional中设置只读

@Transactional注解中,可以通过设置readOnly属性来指定事务是否为只读。例如:

@Transactional(readOnly = true)
public void someMethod() {
    // 业务逻辑
}

在这个例子中,someMethod方法的事务被设置为只读。这意味着,在这个事务中,我们只能进行数据的查询操作,不能进行数据的修改操作。

需要注意的是,这个只读设置只是一个提示,具体的行为可能会根据实际的事务管理器的实现有所不同。例如,某些事务管理器可能会在只读事务中允许进行数据的修改操作,但这通常不是一个好的做法,因为它可能会导致数据的不一致。所以,当我们将一个事务设置为只读时,我们应该确保在事务中不进行任何数据的修改操作。

7. @Transactional的回滚规则

在Spring的@Transactional注解中,可以通过设置rollbackFornoRollbackFor属性来自定义事务的回滚规则。回滚规则决定了在哪些异常情况下事务需要回滚,哪些异常情况下事务可以继续提交。

7.1 默认的回滚规则是什么

默认情况下,@Transactional注解的回滚规则是:当事务中发生运行时异常(RuntimeException)或者错误(Error)时,事务会回滚;当发生受检异常(Checked Exception)时,事务不会回滚。

7.2 如何自定义回滚规则

@Transactional注解中,可以通过设置rollbackFornoRollbackFor属性来自定义事务的回滚规则。例如:

@Transactional(rollbackFor = {CustomException.class}, noRollbackFor = {AnotherCustomException.class})
public void someMethod() {
    // 业务逻辑
}

在这个例子中,我们为someMethod方法自定义了回滚规则:

  1. 当方法抛出CustomException异常时,事务会回滚。这里的CustomException可以是任何继承自Throwable的自定义异常类。
  2. 当方法抛出AnotherCustomException异常时,事务不会回滚。同样,AnotherCustomException可以是任何继承自Throwable的自定义异常类。

通过这种方式,我们可以根据实际需求灵活地自定义事务的回滚规则。需要注意的是,如果同时设置了rollbackFornoRollbackFor,那么noRollbackFor的优先级更高,即如果一个异常同时满足rollbackFornoRollbackFor的条件,那么事务不会回滚。

8. @Transactional的常见问题

在使用@Transactional注解时,可能会遇到一些常见的问题。本节将介绍@Transactional不生效的原因及解决办法,以及使用@Transactional的注意事项。

8.1 @Transactional不生效的原因及解决办法

8.1.1 原因1:事务管理器配置错误或缺失

如果事务管理器没有正确配置或者缺失,@Transactional注解将无法正常工作。

解决办法:确保已经正确配置了事务管理器。对于Spring Boot项目,通常会自动配置事务管理器。对于非Spring Boot项目,需要手动配置事务管理器,例如:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

8.1.2 原因2:使用@Transactional注解的方法在同一个类中调用

当一个没有使用@Transactional注解的方法调用同一个类中的使用@Transactional注解的方法时,事务注解不会生效。

解决办法1:将使用@Transactional注解的方法移到另一个类中,然后在原类中通过依赖注入的方式调用这个方法。例如:

@Service
public class AService {
    @Autowired
    private BService bService;

    public void methodA() {
        bService.methodB();
    }
}

@Service
public class BService {
    @Transactional
    public void methodB() {
        // 业务逻辑
    }
}

解决办法2:将使用 SpringContextUtil.getBean(this.getClass()).methodB()从spring中再获取一边当前bean的代理对象并调用。例如:

@Service
public class BService {
    public void methodA() {
        SpringContextUtil.getBean(this.getClass()).methodB();
    }

    @Transactional
    public void methodB() {
        // 业务逻辑
    }
}

在Spring中,当一个类中的一个方法调用另一个带有@Transactional注解的方法时,通常情况下,@Transactional注解不会生效。这是因为Spring事务管理是基于代理的,当一个方法内部调用另一个方法时,实际上是在同一个对象实例上进行的调用,而不是通过代理对象。因此,事务管理功能不会被触发。

然而,通过使用SpringContextUtil.getBean(this.getClass()).methodB();这种写法,我们可以绕过这个限制。这是因为SpringContextUtil.getBean(this.getClass())会从Spring容器中获取当前类的代理对象,然后通过这个代理对象调用methodB()方法。由于调用是通过代理对象进行的,因此@Transactional注解会生效,触发事务管理功能。

请注意,这种写法虽然可以解决问题,但并不是最佳实践。在实际项目中,我们应该尽量避免在一个类的方法中直接调用另一个带有@Transactional注解的方法。更好的做法是将这两个方法分别放在不同的服务类中,然后通过依赖注入的方式来调用这些服务类的方法。这样可以更好地遵循单一职责原则,使代码更加清晰和可维护。

8.1.3 原因3:事务传播行为配置不正确

如果事务传播行为配置不正确,可能会导致@Transactional注解不生效。

解决办法:检查@Transactional注解的propagation属性设置,确保它符合实际需求。根据不同的业务场景,选择合适的事务传播行为。

8.2 @Transactional的使用注意事项

  1. 确保事务管理器配置正确。对于Spring Boot项目,通常会自动配置事务管理器。对于非Spring Boot项目,需要手动配置事务管理器。

  2. 避免在同一个类中调用使用@Transactional注解的方法。将使用@Transactional注解的方法移到另一个类中,然后在原类中通过依赖注入的方式调用这个方法。

  3. 根据实际需求,合理设置事务的传播行为、隔离级别、超时时间、只读属性等。

  4. 不要在privateprotectedfinal方法上使用@Transactional注解,因为这些方法无法被代理,导致@Transactional注解无法生效。

  5. 如果需要自定义事务的回滚规则,可以通过设置rollbackFornoRollbackFor属性来实现。注意noRollbackFor的优先级更高。

  6. 在使用@Transactional注解时,尽量遵循最小化事务范围的原则,即只在需要进行事务控制的方法上使用@Transactional注解。这样可以降低事务的开销,提高系统性能。

假设我们正在开发一个电商系统,系统中有一个订单服务(OrderService)和库存服务(InventoryService)。当用户下单时,需要同时更新订单和库存信息。为了保证数据的一致性,我们可以使用@Transactional注解来确保这两个操作在同一个事务中完成。如果其中一个操作失败,整个事务将回滚,从而避免数据不一致的问题。

首先,我们创建订单服务(OrderService)和库存服务(InventoryService):

@Service
public class OrderService {
    public void createOrder() {
        // 创建订单的逻辑
    }
}

@Service
public class InventoryService {
    public void updateInventory() {
        // 更新库存的逻辑
    }
}

9.实例

接下来,我们创建一个业务服务(BusinessService),该服务将负责调用订单服务和库存服务。在该服务中,我们使用@Transactional注解来确保这两个操作在同一个事务中完成:

@Service
public class BusinessService {
    @Autowired
    private OrderService orderService;

    @Autowired
    private InventoryService inventoryService;

    @Transactional
    public void placeOrder() {
        try {
            orderService.createOrder();
            inventoryService.updateInventory();
        } catch (Exception e) {
            // 处理异常,例如记录日志等
            throw e;
        }
    }
}

现在,当我们调用BusinessServiceplaceOrder方法时,订单创建和库存更新操作将在同一个事务中进行。如果其中一个操作失败,整个事务将回滚,从而确保数据的一致性。

这个例子展示了如何使用@Transactional注解解决实际问题。在实际项目中,我们可以根据业务需求灵活地使用@Transactional注解来控制事务,确保数据的一致性和完整性。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐