【Spring】事务和@Transactional注解

1.事务概念

Spring 事务管理是一种确保数据一致性和完整性的机制。它允许开发者在操作数据库时将多个步骤封装在一个事务中,要么全部成功,要么在出错时全部回滚。Spring 提供了声明式和编程式事务管理方式,通常使用 @Transactional 注解进行声明式事务管理,以简化事务处理。事务具有四个基本特性:原子性、一致性、隔离性和持久性(ACID),确保在并发环境中数据的可靠性。


事务基本特性(ACID 原则)

  • 原子性 (Atomicity)
    事务是一个不可分割的操作单位,要么全部执行成功,要么全部不执行。原子性确保了在事务执行过程中,若发生错误,所有已执行的操作都会被回滚,数据库将恢复到事务开始之前的状态。

  • 一致性 (Consistency)
    在事务执行前后,数据库必须保持一致性状态。也就是说,事务的执行不会破坏数据的完整性约束。完成一个事务后,数据库的状态应该是有效的,不论是在事务开始前还是结束后。

  • 隔离性 (Isolation)
    隔离性确保多个事务并发执行时,互不干扰。一个事务的执行不应受到其他事务的影响。隔离级别决定了事务间的可见性,通常分为以下几种:

    • 读未提交 (Read Uncommitted)
    • 读已提交 (Read Committed)
    • 可重复读 (Repeatable Read)
    • 序列化 (Serializable)
  • 持久性 (Durability)
    一旦事务被提交,其结果是永久性的,即使系统崩溃也不会丢失。持久性保证了数据库在事务提交后的数据将被持久保存,通常通过日志或其他持久存储机制实现。


事务隔离级别

  • 读未提交 (Read Uncommitted)
    描述:事务可以读取其他事务未提交的数据。这可能导致脏读。
    特点:最低的隔离级别,性能较高,但数据不一致的风险最大。
  • 读已提交 (Read Committed)
    描述:事务只能读取已经提交的数据。未提交的数据对其他事务不可见。
    特点:避免了脏读,但可能发生不可重复读,即同一查询在同一事务内可能返回不同的结果。
  • 可重复读 (Repeatable Read)
    描述:在同一事务内,多次读取相同的数据结果保持一致。这防止了不可重复读。
    特点:在较高的隔离级别下,可能仍然会发生幻读,即在事务执行期间,其他事务可能插入新记录,导致查询结果变化。
  • 串行化 (Serializable)
    描述:最高的隔离级别,确保事务串行执行,完全避免脏读、不可重复读和幻读。
    特点:性能较低,因为它强制事务排队执行,但确保数据一致性。

会出现的问题

  • 脏读 (Dirty Read)
    定义:脏读发生在一个事务读取了另一个事务尚未提交的数据。这意味着如果未提交的事务最终被回滚,那么读取的数据可能是不正确的。
    示例:事务 A 更新某条记录的值,但尚未提交;此时,事务 B 读取了这个未提交的值。如果事务 A 随后回滚,那么事务 B 读取的数据将不再有效。
  • 幻读 (Phantom Read)
    定义:幻读发生在一个事务在同一执行中多次查询同样的条件,但在两次查询之间,其他事务插入或删除了满足查询条件的记录,导致查询结果不同。
    示例:假设事务 A 执行了一次查询,返回了 10 条记录。然后,事务 B 插入了一条新记录,满足了事务 A 的查询条件。当事务 A 再次执行同样的查询时,返回的记录数将变成 11 条,这就是幻读。
  • 不可重复读 (Non-repeatable Read)
    定义:不可重复读发生在一个事务在读取某条记录后,另一个事务对该记录进行了修改或删除,当第一个事务再次读取该记录时,得到的结果与第一次不同。
    示例:假设事务 A 读取某条记录的值为 100。此时,事务 B 对这条记录进行了更新,将其值改为 200。如果事务 A 之后再次读取这条记录,它将得到值 200,而不是最初读取的 100。这种情况就是不可重复读。

2. @Transactional

@Transactional 注解是 Spring Framework 中用于声明式事务管理的关键注解。它允许开发者在方法上或者类上标记事务的边界,使得 Spring 在执行这些方法时自动管理数据库事务。

2.1基本用法

@Transactional 可以标注在类上或者方法上:

类上:表示该类中的所有public方法都应该在一个事务中执行。
方法上:仅表示该方法在事务中执行,类中的其他方法不受影响。


2.2 属性详解

@Transactional 提供了多个属性,帮助开发者更精细地控制事务的行为:

  • value:指定事务管理器的名称。如果没有指定,默认使用唯一的事务管理器。

  • propagation:指定事务的传播行为,默认为 Propagation.REQUIRED,表示如果存在一个事务,则加入这个事务;如果没有,则创建一个新的事务。

    • Propagation.REQUIRED
      说明:如果当前存在事务,则加入该事务;如果没有,则创建一个新的事务。
      示例:方法 A 调用方法 B。如果方法 A 在事务中,方法 B 也在同一个事务中执行;如果方法 A 没有事务,方法 B 会创建一个新的事务

    • Propagation.SUPPORTS
      说明:如果当前存在事务,则加入该事务;如果没有,则以非事务方式执行。
      示例:方法 A 调用方法 B。如果方法 A 在事务中,方法 B 将在同一事务中执行;如果方法 A 没有事务,方法 B 将以非事务方式执行。

    • Propagation.MANDATORY
      说明:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
      示例:方法 A 调用方法 B。如果方法 A 没有事务,调用将失败并抛出异常;如果有,则方法 B 在该事务中执行。

    • Propagation.REQUIRES_NEW
      说明:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。
      示例:方法 A 调用方法 B。如果方法 A 有事务,方法 B 将创建一个新的事务,方法 A 的事务会被挂起。两者的事务相互独立。 A方法中调用 B方法操作数据库, A方法抛出异常后,B方法不会进行回滚。

    • Propagation.NOT_SUPPORTED
      说明:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
      示例:方法 A 调用方法 B。如果方法 A 有事务,方法 B 将在非事务状态下执行,方法 A 的事务会被挂起。

    • Propagation.NEVER
      说明:以非事务的方式运行,如果当前存在事务,则抛出异常。
      示例:方法 A 调用方法 B。如果方法 A 有事务,将抛出异常;如果没有事务,则正常执行。

    • Propagation.NESTED
      说明:如果当前存在事务,则创建一个嵌套事务;如果没有事务,则行为与 PROPAGATION_REQUIRED 相同。
      示例:方法 A 调用方法 B。如果方法 A 有事务,方法 B 将创建一个嵌套事务,可以独立提交或回滚。如果方法 A 没有事务,则方法 B 会创建一个新事务。

  • isolation:指定事务的隔离级别,默认为 Isolation.DEFAULT。常用的隔离级别有:

    • Isolation.READ_UNCOMMITTED
    • Isolation.READ_COMMITTED
    • Isolation.REPEATABLE_READ
    • Isolation.SERIALIZABLE
  • timeout:设置事务的超时时间,单位为秒。超过该时间,事务会被强制回滚。

  • readOnly:设置为 true 表示该事务是只读的,通常用于查询操作,优化性能。

  • rollbackFor:指定哪些异常会导致事务回滚。可以是异常类或异常类的数组。

  • noRollbackFor:指定哪些异常不会导致事务回滚。


代码示例:

public class UserService {

    @Transactional(
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.READ_COMMITTED,
        readOnly = true,
        timeout = 5, // 事务超时时间
        rollbackFor = Exception.class // 指定回滚的异常类型
    )
    public User getUserById(Long id) {
        // 查询用户
        ...
    }
}

3.事务在函数间的互相调用

3.1 同一个类中函数的相互调用

场景:A类中,有两个方法,方法A1和A2;方法A1调用方法A2;方法A1被其他类调用。

示例1:方法A1和A2都添加@Transactional注解

@Service
public class AClass {

    @Transactional(rollbackFor = Exception.class)
    public void A1() {
        // 业务逻辑
        A2(); 
    }

	@Transactional
    public void A2() {
        // 执行数据库操作,假设抛了异常
      	// ...
        throw new RuntimeException("函数异常!");
    }
}

结果:A1被注解为 @Transactional,那么在 A1被调用时会开启一个事务。由于 A1内部调用 A2是在同一个实例内,A2会在同一个事务上下文中执行,事务会正常生效,同类调用,不涉及事务传播,相当于A2的代码加到了A1方法内。
整个调用过程都在 A1的事务控制下,包括对 A2的调用。这意味着,如果 A1中发生异常,事务将会回滚,包括 A2的操作。
如果 A1被标记为 @Transactional,而 A2没有被标记为事务,则事务依然会生效,因为它是在同一个事务上下文中运行。


示例2:方法A1不添加@Transactional注解,A2添加@Transactional注解

@Service
public class AClass {

    public void A1() {
        // 业务逻辑
        A2(); 
    }

	@Transactional(rollbackFor = Exception.class)
    public void A2() {
        // 执行数据库操作,假设抛了异常
      	// ...
        throw new RuntimeException("函数异常!");
    }
}

结果:当其他类调用 A1时,A1运行在没有事务的上下文中。随后在 A1中调用 A2,由于 A2是在 A1的上下文中直接调用的,所以它不会启动一个新的事务。
在这种情况下,A2的 @Transactional 注解不会生效,因为 Spring 的事务管理机制依赖于代理模式,同类方法调用不会调用代理对象的方法。只有当事务方法通过 Spring 的代理进行调用时,事务管理才会生效。因此,A2中的数据库操作将会直接执行,而不受事务控制。
如果 A2中发生异常,A1也不会回滚,因为 A1本身没有事务。如果需要进行回滚,aFunction 应该被标记为 @Transactional


3.2 不同类中函数的相互调用

场景:两个类分别为A类、B类;A类有方法A、B类有方法B;A类方法A调用B类方法B;A类方法A被另一个C类调用。

示例1:方法A添加注解,方法B不添加注解

@Service
public class AClass {
    @Autowired
    private BClass bClass;
 
    @Transactional(rollbackFor = Exception.class)
    public void A() {
        // 执行数据库操作
        bClass.B();
    }
}
 
@Service
public class BClass {
 
    public void B() {
        // 执行数据库操作,假设抛了异常
      	// ...
        throw new RuntimeException("函数执行有异常!");
    }
}

结果:两个函数对数据库的操作都回滚了,因为 A被标记为 @Transactional,当它被调用时,会开启一个事务,当 A调用 B时,因为 B没有事务控制,所以它将直接在当前的事务上下文中执行。如果 B抛出异常,异常将会传播回 A,因为 A处于事务管理之下,如果 B抛出一个异常,A将会捕获到这个异常,并且会导致整个事务回滚。


示例2:方法A不添加注解,方法B添加注解

@Service
public class AClass {
    @Autowired
    private BClass bClass;
 
    public void A() {
        // 执行数据库操作
        bClass.B();
    }
}
 
@Service
public class BClass {
 
  	@Transactional(rollbackFor = Exception.class)
    public void B() {
        // 执行数据库操作,假设抛了异常
      	// ...
        throw new RuntimeException("函数执行有异常!");
    }
}

结果:A、B都不会回滚。因为方法 A 运行在没有事务的上下文中,方法 B 的 @Transactional 注解不会生效,如果方法 B 抛出异常,方法 A 也会捕获到这个异常,但不会导致任何事务回滚,因为方法 A 没有事务控制。


示例3:A、B两个函数都添加事务注解;B抛异常;A抓出异常

@Service
public class AClass {
    @Autowired
    private BClass bClass;
 
    @Transactional(rollbackFor = Exception.class)
    public void A() {
        try {
        	// 执行数据库操作
            bClass.B();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
}
 
@Service
public class BClass {
    @Transactional(rollbackFor = Exception.class)
    public void B() {
        // 执行数据库操作,假设抛了异常
      	// ...
        throw new RuntimeException("函数异常!");
    }
}

结果:整个事务不会提交,且抛异常org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,所有在方法A和方法B中的数据库操作都会被回滚。

因为两个函数用的是同一个事务;B函数抛了异常,调了事务的rollback函数,并且事务被标记了只能rollback了;程序继续执行,A函数里面把异常给抓出来了,这个时候A函数没有抛出异常,既然没有异常那事务就需要提交,会调事务的commit函数;而之前这个事务已经被标记了只能rollback-only(因为是同一个事务),因此直接就抛异常了。


示例4:A、B两个函数都添加注解;B抛异常,A抓出异常;这里B函数@Transactional注解加了一个参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为,表明是一个新的事务

@Service
public class AClass {
    @Autowired
    private BClass bClass;
 
    @Transactional(rollbackFor = Exception.class)
    public void A() {
        try {
        	// 执行数据库操作
            bClass.B();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
}
 
@Service
public class BClass {
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void B() {
        // 执行数据库操作,假设抛了异常
      	// ...
        throw new RuntimeException("函数异常!");
    }
}

结果:B函数里面的操作回滚了,A函数里面的操作成功了;因为两个函数不是同一个事务了,所以B函数抛异常只会导致B的回滚,不影响A所在事务的正常执行。


4.事务失效场景

4.1.同一类内部方法调用

场景: 如果在同一个类内部的方法中调用被 @Transactional 注解的方法,事务可能不会生效。
解决办法: 将事务方法放在不同的服务类中,或者使用 Spring 的 AOP 代理机制,使其能够通过代理进行调用。

4.2 代理对象调用

场景: 当通过非代理对象(例如直接使用 this)调用 @Transactional 方法时,事务将失效。
解决办法: 确保通过 Spring 容器管理的代理对象调用事务方法,避免使用 this。

4.3 异常类型

场景: 默认情况下,只有未检查的异常(RuntimeException)会导致事务回滚,检查异常不会导致回滚。
解决办法: 如果需要针对检查异常进行回滚,可以使用 rollbackFor 属性指定异常类型。

4.4 事务传播行为

场景: 选择不当的事务传播行为可能导致事务失效,例如使用 PROPAGATION_NOT_SUPPORTED 会以非事务方式执行。
解决办法: 根据业务需求选择合适的传播行为,确保方法调用之间的事务关系。

4.5.嵌套事务

场景: 嵌套事务可能不会按照预期工作,特别是在使用 PROPAGATION_NESTED 时。
解决办法: 了解嵌套事务的行为,并确保使用合适的事务传播策略。

4.6 多线程环境

场景: 在多线程环境中,事务可能无法正常传播。
解决办法: 确保在同一线程中处理事务,或者使用合适的方式管理跨线程事务。

你可能感兴趣的:(Spring,spring,java)