介绍
在处理数据库时,了解事务边界对于维护数据完整性和一致性至关重要。Java 的 Spring 框架提供了一个名为 Spring 的强大工具@Transactional来控制这些边界。此注释不仅仅是启动和提交/回滚事务的机制,而且它通过其属性提供了更多控制。开发人员需要了解交易的两个关键方面是传播和隔离。本文深入探讨了这些方面,阐明了如何@Transactional在 Spring 应用程序中有效使用。在这边问我想向大家推荐一本Springboot实战派,从中可以看到Springboot的整个实现原理,与源码解读。
@Transactional 简介
在企业应用程序领域,事务在确保操作的原子性、一致性、隔离性和持久性(通常称为 ACID 属性)方面发挥着关键作用。这些属性确保我们的系统即使在遇到故障时也能保持一致的状态。
Spring 框架因其简化 Java 开发的综合工具包而闻名,它提供@Transactional方法级别的事务行为管理。让我们来解开这意味着什么:
了解事务上下文
事务的本质是将多个操作封装为一个单元。如果所有操作都成功,则称事务已提交。即使其中一项操作失败,整个事务也会回滚,就好像所有操作都没有发生一样。注释@Transactional有助于设置方法周围的边界。当您使用 注释方法时@Transactional,您实际上是在告诉 Spring 容器该方法中的所有内容都应被视为单个事务单元。
声明式事务管理
在 Spring 中有两种管理事务的方式:编程方式(使用 Transaction API)和声明方式(使用@Transactional注释或 XML 配置)。后者更受欢迎和推荐,因为它将事务管理排除在业务逻辑之外,从而使代码更干净、更易于维护。
使用@Transactional,您可以以声明方式定义事务管理规则。当调用带注解的方法时,Spring会动态创建一个代理,根据该方法的执行来处理事务的创建、提交和回滚。
@Transactional 如何工作?
在底层,当 Spring 遇到注释时@Transactional,它会围绕实际 bean 动态创建一个代理对象。该代理负责管理事务边界。如果该方法成功完成,它将提交事务。如果存在任何未经检查的异常,它将回滚事务。对于检查异常,它依赖于我们设置的配置(默认情况下,它不会回滚检查异常)。
回滚事务
一种常见的误解是@Transactional仅回滚未经检查的异常(RuntimeException的子类)。默认情况下,这是正确的。但是,您可以自定义此行为。如果需要事务在特定的已检查异常上回滚,可以使用 的属性来rollbackFor指定@Transactional。
public class UserService { private UserRepository userRepository; (rollbackFor = UserNotFoundException.class) public User updateUserDetails(long userId, User updatedDetails) throws UserNotFoundException { User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("User not found")); user.setName(updatedDetails.getName()); user.setEmail(updatedDetails.getEmail()); return userRepository.save(user); } }
在上面的示例中,即使UserNotFoundException是检查异常,如果抛出事务也会回滚。
范围和边界
虽然@Transactional可以在类和方法上使用,但明智地使用它至关重要。如果您注释一个类,则该类中的每个公共方法都将成为事务性的。但是,如果您注释特定方法,则只有这些方法具有事务上下文。这允许对代码的哪些部分应该是事务性的进行细粒度控制。
传播:交易如何表现
在 Spring 中,传播行为决定事务方法如何与现有事务交互。了解这一点非常重要,因为不正确的传播设置可能会导致细微的错误、数据不一致和性能问题。
为什么传播很重要
想象这样一种场景,您有多个事务方法,这些方法可以独立调用,也可以相互调用。这些方法如何与现有事务交互或如何启动事务可能会影响整体行为和结果。因此,选择正确的传播策略至关重要。
传播模式解释:
必需的
- 这是默认模式,顾名思义,它需要事务上下文。
- 如果调用该方法时有正在进行的事务,则该方法将加入该事务。
- 如果不存在事务,则开始新的事务。
- 此模式非常适合操作相互依赖且需要一起成功或一起失败的典型用例。
支持
- 该方法可以在事务内部和外部工作。
- 如果存在现有事务,该方法会加入该事务;如果不是,则该方法在没有事务的情况下运行。
- 当方法内的操作不一定需要事务时,这是一种灵活的方法,但如果事务已经在进行中,他们仍然可以从事务中受益。
强制的
- 此模式严格强制存在活动事务。
- 如果没有正在进行的事务,则会引发运行时异常。
- 这是确保调用方法已经启动事务的一种方法。
REQUIRES_NEW
- 此模式确保该方法始终在新事务中运行。
- 如果存在现有事务,则该事务会在该事务开始之前暂停。完成后,外部事务将恢复。
- 当方法中的操作需要与任何外部事务隔离的事务边界时,这非常有用。
不支持
- 即使有正在进行的事务,该方法也将在没有任何事务上下文的情况下执行。
- 任何活动事务都会在该方法执行之前暂停,然后再恢复。
- 当某些操作不应该是事务性的(尽管是在事务性上下文中调用)时很有用。
绝不
- 此模式确保不存在事务上下文。
- 如果有正在进行的事务,则会引发运行时异常。
- 它保证方法内的操作始终在没有事务的情况下运行。
嵌套
- 如果存在活动事务,则该方法将在该现有事务的嵌套事务中运行。
- 嵌套事务是一组可以独立提交或回滚的操作,同时仍然是较大事务的一部分。
- 这很复杂,需要仔细考虑。并非所有事务管理器都支持此模式。
传播模式的实际意义
了解这些模式至关重要,但了解何时使用它们更为重要。让我们举一个简单的例子。
假设您有一个用于下订单的电子商务应用程序,并且您还需要更新库存。你可能有方法placeOrder和updateStock。如果updateStock失败,您可能不想下订单。在这种情况下,placeOrder可以启动一个新事务(使用REQUIRED)并updateStock可以加入正在进行的事务(REQUIRED也使用)。如果任何方法失败,整个事务将回滚。
但是,如果您希望updateStock其交易即使失败,下单也不会回滚,那么updateStock应该使用REQUIRES_NEW.
隔离:保持数据完整性
使用数据库时,尤其是在多个事务同时运行的系统中,由于多个事务操作访问同一组数据,数据完整性可能会面临风险。隔离级别确定一个事务中的操作与其他并发事务中的操作隔离的程度。
隔离的必要性
在多用户或多服务环境中,同时执行事务时可能会出现各种问题:
- 脏读:一个事务读取另一事务未提交的更改。
- 不可重复读取:在单个事务中,相同的查询在不同时间产生不同的结果。
- 幻读:一个事务读取了另一个事务已插入或删除的行,导致重复读取结果不一致。
为了管理这些问题,数据库提供了各种隔离级别。通过了解并设置适当的级别,您可以在数据完整性和系统性能之间取得平衡。
Spring @Transactional 中的隔离级别
Spring 的@Transactional注释允许您设置与标准 SQL 隔离级别紧密一致的隔离级别:
默认
- 它依赖于底层数据存储的默认隔离级别。
- 除非需要特定的隔离行为,否则这通常是一个安全的选择。
READ_UNCOMMITTED
- 这是最低级别的隔离。
- 事务可能会读取其他事务中未提交的更改,从而导致脏读。
- 虽然它提供了更好的性能,但它可能会损害数据完整性。
已提交读
- 事务只能读取已提交的更改。
- 它可以防止脏读,但仍然允许不可重复读。
- 这是一种常用的隔离级别,因为它提供了性能和数据一致性之间的平衡。
可重复读取
- 保证如果在同一个事务中多次读取某个值,结果将始终相同。
- 它可以防止脏读和不可重复读,但仍然允许幻读。
可串行化
- 最高级别的隔离。
- 确保与其他事务完全隔离,防止脏读、不可重复读和幻读。
- 虽然它提供了最强大的数据完整性保证,但由于采用了更严格的锁,它可能会变慢。
选择正确的隔离级别
选择隔离级别通常需要考虑数据完整性要求和系统的性能期望。以下是一些指导原则:
- 如果数据完整性至关重要并且您可以容忍潜在的性能开销,那么这SERIALIZABLE可能是最佳选择。
- 在某些情况下,为了获得更好的性能,偶尔的不一致是可以接受的,READ_COMMITTED甚至READ_UNCOMMITTED可能是合适的。
- 对于大多数典型的用例,READ_COMMITTED提供了良好的平衡。
例子:
public class FinancialService { private TransactionRepository transactionRepository; (isolation = Isolation.READ_COMMITTED) public void transferFunds (Account from, Account to, double amount) { // 资金转账的业务逻辑 } }
结合传播和隔离
当我们谈论事务管理时,传播决定了事务的“边界”或“生命周期”,而隔离则定义了事务操作对其他并发事务的“可见性”。当组合起来时,它们共同塑造系统的事务行为,决定性能和数据完整性。
为什么要结合两者?
在某些情况下,仅设置传播或隔离是不够的。例如,您可能希望一个方法始终在其事务中执行(使用REQUIRES_NEW),但根据所涉及的业务逻辑具有不同的隔离级别。
常见场景
让我们探讨一些将两者结合起来会带来好处的情况:
高风险操作:
- 对于金融交易等关键操作,您可能希望该方法始终在新事务中运行,以避免对其他操作产生任何干扰 ( REQUIRES_NEW)。此外,为了确保高数据完整性,您可能会选择更高的隔离级别,例如SERIALIZABLE.
批量处理:
- 在批处理过程中,块操作很常见。每个块可能在其事务中运行,但不应看到其他块的未提交工作。在这里,REQUIRES_NEW传播和READ_COMMITTED隔离可以是一个恰当的组合。
报告操作:
- 对于数据一致性不是主要关注点但性能才是主要关注点的报告或分析,操作可以加入现有事务 ( SUPPORTS),并且可以在较低的隔离度上进行操作,例如READ_UNCOMMITTED更快地获取数据。
Spring如何搭配
在 Spring 中,使用注释组合这两个设置非常简单@Transactional:
public class FinancialService { private TransactionRepository transactionRepository; (propagation = Propagation.REQUIRES_NEW,isolation = Isolation.SERIALIZABLE) public void highStakesOperation (Account from, Account to, double amount) { // 这里的业务逻辑 } }
组合时的注意事项
虽然组合这些设置可以提供对事务的强大控制,但必须谨慎:
- 死锁:较高的隔离级别(尤其是)SERIALIZABLE会增加系统中死锁的风险,尤其是与某些传播行为结合使用时。
- 性能:在确保数据完整性的同时,较高的隔离级别会影响性能。始终在实际负载下对系统进行分析和基准测试,以确保组合不会过度降低性能。
- 复杂性:自定义事务行为越多,维护和调试问题就越困难。确保团队充分了解所选设置并理解其含义。
结论
控制事务边界对于确保应用程序中的数据一致性和完整性至关重要。Spring框架在@Transactional注释中提供了一个强大的工具,允许开发人员微调事务的传播和隔离方面。通过理解并有效利用这些控件,开发人员可以构建健壮且容错的数据库驱动应用程序。