0
点赞
收藏
分享

微信扫一扫

开发语言漫谈-Object C

前程有光 2024-04-22 阅读 38

什么是事务?

事务(Transaction)是数据库系统中用于保证数据一致性和完整性的基本单位。遵循 ACID 的特性。

  • 原子性(Atomicity):

事务被视为一个不可分割的工作单元,要么全部成功执行,要么全部失败回滚。这意味着事务中的所有操作要么全部完成,要么全部撤销,不会出现部分完成的情况。原子性保证了数据的完整性,避免了因部分操作失败导致的数据不一致。

  • 一致性(Consistency):

事务执行前后,数据库必须从一个有效状态变为另一个有效状态,始终保持数据的逻辑一致性。一致性确保了事务对数据的修改符合预先定义的业务规则和约束条件,如数据完整性约束、业务逻辑约束等。无论事务执行成功还是失败,都不会破坏数据的内在逻辑。

  • 隔离性(Isolation):

并发执行的事务之间互不影响,如同串行执行一样。隔离性通过锁定、版本控制等机制,防止多个事务同时访问相同数据时产生相互干扰,导致数据不一致。根据隔离级别不同,可以提供不同程度的并发访问保护,如READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。

  • 持久性(Durability):

一旦事务成功提交,其对数据库的修改将永久保存,即使发生系统崩溃、电源故障等情况,已经提交的事务结果也不会丢失。持久性通过事务日志、数据备份等机制,确保事务的最终结果能够持久化到稳定的存储介质上,确保数据的可靠性。

理解事务,还需要知道事务的生命周期:

  • 开始(Begin):事务开始执行,系统为事务分配必要的资源并记录事务的初始状态。

  • 执行(Execute):事务执行一系列数据操作,如查询、插入、更新、删除等。在这个过程中,系统会维护事务的中间状态,如临时锁定资源、记录回滚日志等。

  • 提交(Commit):事务执行完毕,系统检查事务是否满足原子性、一致性等要求。如果满足,事务被永久提交,其对数据的修改成为数据库的永久状态;如果不满足,事务被回滚,所有操作撤销,数据库恢复到事务开始前的状态。

  • 回滚(Rollback):当事务执行过程中遇到错误,或者用户主动要求回滚,系统会撤销事务执行的所有操作,释放锁定的资源,恢复数据库到事务开始前的状态。

Spring @Transactional 实现原理

Spring 的 @Transactional 是基于 AOP(面向切面编程)实现的,在方法执行时,通过代理技术将方法拦截后,新增一个事务切面。在事务切面中,可以调用底层的事务管理器,以保证事务的一致性。

在 Spring 中,有两种事务管理方式:

  1. 编程式事务管理:通过在代码中显式调用事务管理方法来实现事务管理。

  2. 声明式事务管理:通过配置文件或注解的形式来声明事务管理,实现对事务的自动管理。

Spring 中的 @Transactional 属于声明式事务管理。它由 TransactionInterceptor 拦截器和 TransactionAttributeSource 事务属性源组成。

TransactionInterceptor 拦截器是实现了 JDK 动态代理和 AOP 的类,当调用一个被 @Transactional 注解的方法时,会根据 @Transactional 注解的参数去获取对应的事务属性,然后通过 TransactionManager 来开启一个事务,并将事务信息保存到当前线程的 ThreadLocal 中。

在方法执行过程中,如果发生异常,则事务管理器会回滚事务,否则事务提交并关闭。

简言之,@Transactional 的实现原理就是通过代理技术在方法执行过程中,拦截方法调用并添加事务切面,然后根据事务属性进行事务管理。

Spring 中事务失效的场景

  • 在同一个类内部直接调用,而不是通过接口调用,@Transactional 注解不会生效。
@Service
public class ServiceA {
    @Resource
    private UserMapper userMapper;
    public void func1(){
        func2();
    }
    @Transactional(rollbackFor = Exception.class)
    public void func2(){
        User user = User.builder().name("lisi" + new Random().nextInt()).password("123456").phone("dsf12313").address("").build();
        userMapper.insert(user);
        int ret = 1/0;
    }
}
  • 使用注解作用在没有被 public 修饰的方法上也会失效
    如下:非 public 方法会被 IDEA 标记,因为 Spring 要求被代理的方法必须是 public.
    在这里插入图片描述

  • 多个 sevice调用情况
    情况 1:
    serviceA func1 -> func2(事务标记)
    serviceB 的 func1 调用 serviceA func1 – 事务失效,原因是serviceA 存在同类中非事务方法调用了事务方法。

    情况 2:
    serviceB 的 func1 调用 serviceA func2(含事务标记) —事务正常

    情况 3:
    serviceA func2(事务标记) ,func3(事务标记 并伴有异常)
    serviceB 的 func1 调用serviceA func2,func3. —>结果:事务正常运行

  • 手动进行异常处理导致的事务失效

    @Transactional(rollbackFor = Exception.class)
    public void func1() throws IOException {
        User user = User.builder().name("c" + new Random().nextInt(1000))
                .password("123123").phone("1233131").address("42342").build();
        userMapper.insert(user);
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
   }

Spring事务传播属性

在这里插入图片描述

1. 会创建新事务的传播行为有:
  • PROPAGATION_REQUIRED(默认行为):

    如果当前存在事务,方法加入到当前事务中,不会创建新事务。
    如果当前没有事务,会创建一个新的事务。

  • PROPAGATION_REQUIRES_NEW:

    始终创建一个新的事务,无论当前是否存在事务。
    如果当前存在事务,先将当前事务挂起,新方法在新创建的事务中执行。新事务提交或回滚后,恢复并继续执行原来的事务。

  • PROPAGATION_NESTED(嵌套事务):
    如果当前存在事务,会创建一个嵌套事务(保存点),嵌套事务内部的提交或回滚不会影响外部事务,只有当外部事务也提交时,嵌套事务的变更才会真正持久化。如果外部事务回滚,嵌套事务将随之回滚到其保存点。
    如果当前没有事务,PROPAGATION_NESTED 行为等同于 PROPAGATION_REQUIRED,即创建一个新的事务。

2. 不会创建新事务,否则会抛出异常

PROPAGATION_NEVER 和 PROPAGATION_NOT_SUPPORTED 传播行为不会创建新事务,而是要求当前不能有事务存在。如果存在事务,它们会抛出异常。

3. SUPPORTS可以在有事务和非事务的环境中运行,始终不会创建新的事务

PROPAGATION_SUPPORTS 表示方法可以加入当前事务(如果存在),也可以在没有事务的环境中执行,都不会创建新事务。

4.必须要在当前事务中运行

PROPAGATION_MANDATORY 方法必须在现有事务中执行,否则抛出异常。

一些问题

1、为什么 spring 声明式事务的隔离级别是 default ?

DEFAULT 表示使用数据库系统的默认事务隔离级别。不同的数据库系统(如 MySQL、Oracle、PostgreSQL 等)对事务隔离级别的默认设置可能不同,但通常会选择一个既能保证一定的数据一致性,又能兼顾一定并发性能的级别,如 READ_COMMITTED。
选择 DEFAULT 作为默认值,可以让 Spring 事务管理器与数据库系统的默认行为保持一致,减少配置负担,尤其是对于那些对事务隔离级别要求不严格的常规业务场景。

2、@Transactional 和 @Transactional(rollbackFor = Exception.class) 到底有什么区别?

@Transactional(不带 rollbackFor 属性):

默认行为:对于非检查异常(继承自 java.lang.RuntimeException 的异常,包括 java.lang.RuntimeException 本身及其子类,如 NullPointerException、IllegalArgumentException、IllegalStateException 等),Spring 事务管理器会默认自动回滚事务。
检查异常:对于检查异常(继承自 java.lang.Exception 且不是 java.lang.RuntimeException 的子类的异常),Spring 事务管理器默认不会为它们自动回滚事务。这意味着,如果在 @Transactional 方法中抛出了一个检查异常,事务通常不会自动回滚,除非您显式捕获并重新抛出一个非检查异常(如 RuntimeException 或其子类)。

@Transactional(rollbackFor = Exception.class):

显式指定回滚条件:添加了 rollbackFor = Exception.class 参数后,Spring 事务管理器会为所有继承自 java.lang.Exception 类及其子类的异常(包括检查异常和非检查异常)自动回滚事务。这意味着,无论是非检查异常还是检查异常,只要它们是 Exception 类的子类,当它们在 @Transactional 方法中抛出时,事务都会被自动回滚。

3、什么情况下应该使用什么类型的事务传播行为呢?

选择事务传播行为应依据具体的业务场景和数据一致性需求。以下是一些指导原则:

  1. PROPAGATION_REQUIRED(默认)

    • 适用场景:大多数常规业务操作,需要确保方法内所有数据库操作都在同一个事务中。
    • 何时使用:当一个方法内部可能包含多个数据库操作,且这些操作应作为一个原子单元来执行时。例如,创建订单时同时更新库存和用户账户余额。
  2. PROPAGATION_REQUIRES_NEW

    • 适用场景:需要在一个独立的新事务中执行某个方法,不受现有事务影响,且无论外部事务状态如何,新事务都能确保自己的完整性和独立提交。
    • 何时使用
      • 长时间运行的任务:当一个方法可能执行时间较长,为了避免阻塞其他事务或防止超时,可使用新事务执行。
      • 确保数据一致性:在处理敏感操作(如资金转移)时,即使外部事务失败,新事务也能确保自身操作的提交或回滚,不影响其他事务。
      • 避免嵌套事务异常:当外部事务可能引发异常,但仍希望确保内部方法的操作能独立提交时。
  3. PROPAGATION_SUPPORTS

    • 适用场景:方法对事务的参与与否不敏感,既可以加入现有事务,也可以在无事务环境中执行。
    • 何时使用:对于读取操作或只影响缓存、日志等非事务性资源的方法,可以使用此传播行为。这些方法通常不会修改持久化数据,即使在事务外执行也不影响数据一致性。
  4. PROPAGATION_MANDATORY

    • 适用场景:方法必须在现有事务中执行,否则抛出异常。
    • 何时使用:对于那些逻辑上必须在事务上下文中执行的方法,使用此传播行为可以强制进行事务检查,确保事务一致性。
  5. PROPAGATION_NOT_SUPPORTED

    • 适用场景:方法不应在事务环境中执行,若当前存在事务,应暂停(挂起)当前事务。
    • 何时使用:对于那些可能与事务不兼容或不需要事务保障的操作,如清理任务、发送通知邮件等,可以使用此传播行为避免与事务关联。
  6. PROPAGATION_NEVER

    • 适用场景:方法绝对不能在事务环境中执行,若当前存在事务,应抛出异常。
    • 何时使用:极少使用,通常在遇到特定的非事务性资源或操作时,需要严格禁止事务介入。
  7. PROPAGATION_NESTED

    • 适用场景:需要在一个嵌套事务(保存点)中执行方法,内部事务可以独立回滚而不影响外部事务。
    • 何时使用:在复杂业务流程中,某个步骤可能包含可回滚的部分,如订单创建过程中的预扣款操作,如果后续步骤失败,可以仅回滚预扣款而保留其他已完成的步骤。

总的来说,选择事务传播行为时,要考虑方法对事务的依赖程度、操作的原子性要求、与外部事务的交互关系以及可能出现的异常情况。理解每种传播行为的语义并结合业务逻辑来做出最佳选择。

4. 当前线程在调用同一个事务方法还未运行结束此时再次调用该事务方法会创建新的事务吗?

在这种情况下,如果当前线程在调用同一个事务方法并该方法尚未完成执行,而此时再次调用该事务方法,通常情况下不会创建新的事务。相反,后续的调用将会加入到当前已存在的事务中。

这是因为默认情况下,Spring 的事务传播行为为Propagation.REQUIRED,意味着如果当前已经存在一个事务,则方法将会加入到该事务中,而不会创建新的事务。因此,如果在同一个线程中连续调用同一个事务方法,后续的调用会继续沿用当前已经存在的事务,而不会创建新的事务。

这种行为有助于确保事务的一致性和原子性,因为所有的方法调用都在同一个事务的上下文中执行,而不会出现多个相互独立的事务。

5. 新建一个事务的性能开销在哪里?

主要涉及以下几个方面:

  1. 事务管理器的开销: 当一个方法被要求新事务中运行,事务管理器需要执行一系列操作来管理事务的生命周期,包括事务的开始、提交、回滚等操作。这些操作会带来一定的性能开销。

  2. 数据库连接的获取和释放: 每个事务都需要获取一个数据库连接来执行相应的操作,而且在事务结束时需要释放这个连接。频繁创建新事务会导致频繁获取和释放数据库连接,增加了数据库连接管理的开销。

  3. 事务上下文的管理: 每个事务都会创建一个事务上下文,用于保存事务的状态和相关信息。频繁创建新事务会导致频繁创建和管理事务上下文,增加了内存和资源的消耗。

  4. 锁的管理: 在事务中对数据库进行操作时,可能会涉及到对数据库中的数据进行加锁操作,以保证事务的隔离性和一致性。频繁创建新事务会导致频繁对数据库中的数据进行加锁和解锁操作,增加了锁的管理开销。

因此,频繁创建新事务会增加事务管理器、数据库连接、事务上下文和锁管理等方面的性能开销,因此在设计应用程序时需要谨慎考虑事务的粒度和频繁创建事务的情况,以避免不必要的性能损耗。

参考

https://zhuanlan.zhihu.com/p/383829479

举报

相关推荐

0 条评论