Spring | 事务原理与实践 - 声明式事务及编程式事务

2023-09-27 10:28:59 浏览数 (1)

引言

在多用户并发操作和大数据处理的现代软件开发领域中,事务管理已成为确保数据一致性和完整性的关键技术之一。特别是在使用如Spring这样的全面框架时,理解和掌握其事务管理机制不仅有助于我们编写出更为健壮和高效的应用,还能帮助我们避免一些由于事务处理不当带来的问题,如数据不一致性、数据丢失等。

Spring事务管理旨在为Java应用程序提供一个精确、简洁和全面的事务管理解决方案,但实际使用过程中,由于配置、使用方式或理解不当,开发人员经常会遇到一些问题,例如事务失效。因此,本文将详细解析Spring事务的原理、类型和实践,通过深入剖析源码和实际案例,带领读者深入理解Spring事务管理的工作机制,并探讨在复杂业务场景下的事务处理策略和最佳实践。

以下所有示例均已上传至Github上,大家可以将项目拉取到本地进行运行

Github示例(如果对Gradle还不熟练,建议翻看我之前的文章):gradle-spring-boot-demo


一、Spring事务基础

在深入研究Spring事务管理之前,首先我们需要掌握事务的基本概念。事务是由一系列对系统中的数据进行访问和更新的操作组成,应该具有ACID四个基本属性。

1.1 什么是事务

事务是由数据库管理系统在执行过程中形成的一个逻辑单位,它由一组有限的数据库操作序列组成。通常情况下,事务是由程序单元通过高级语言或数据库的数据操作语言提交的。

1.1.1 ACID属性

事务需要满足ACID的四个基本属性,这四个属性确保了事务的稳定性和数据的一致性。

  • 原子性(Atomicity):事务应该是一个 indivisible 的工作单位。事务中包含的操作要么都成功,要么都失败回滚。
  • 一致性(Consistency):事务必须保持数据的一致性,即事务的执行不能破坏数据库的一致性约束。
  • 隔离性(Isolation):并发事务之间应该相互隔离,一个事务的执行不应该被其它事务所干扰。
  • 持久性(Durability):一旦事务提交,对数据的改变就应该是永久性的。
ACID的实现方式

实现ACID的主要有两种方式:

  1. Write Ahead Logging(日志先行写入):在这种方式下,所有的修改都会首先被写入到日志中,然后才会应用到数据库。
  2. Shadow Paging(影子页表)^https://zh.wikipedia.org/wiki/影子分页:这种方式下,数据修改不会直接在原数据上进行,而是通过影子页来实现,保证原数据的安全性和完整性。影子分页就是我们常说的写时复制技术。

1.2 为什么需要事务

事务确保了多个操作作为一个整体来保持数据的一致性和完整性,特别是在并发环境下。

1.2.1 数据一致性

数据一致性要求在事务开始和结束时,数据必须处于一致的状态。这意味着即便在系统发生故障的情况下,通过适当的事务管理,数据的一致性也不会受到影响。

1.2.2 数据完整性

数据完整性要求数据必须满足预定义的业务规则和约束,例如主键和外键约束。通过正确的事务管理,我们可以在事务执行和结束时满足这些约束,从而确保数据的完整性。

1.2.3 事务的隔离级别

事务的隔离级别定义了一个事务可能会受到其他并发事务的影响程度。这里我们用一个简单的类比来理解四个隔离级别:将事务比作在一个多层的大楼中的房间,每个房间的窗户可以打开或关闭。不同的隔离级别就像是窗户开得有多大,决定了能看到大楼外部的多少内容。

下面是四个隔离级别下的不同情况:

隔离级别

脏读

不可重复读

幻读

READ UNCOMMITTED(未提交读)

可能

可能

可能

READ COMMITTED(已提交读)

不可能

可能

可能

REPEATABLE READ(可重复读)

不可能

不可能

可能

SERIALIZABLE(串行化)

不可能

不可能

不可能

  • MySQL中,默认的隔离级别是 REPEATABLE READ
  • Oracle中,默认的隔离级别是 READ COMMITTED

二、Spring事务管理类型

Spring事务管理主要可以分为两种类型:编程式事务管理声明式事务管理。这两种类型提供了不同层次上的事务控制,使得开发者能够在不同的场景下选择最合适的事务管理策略。

2.1 编程式事务管理

编程式事务管理允许你在代码中显式地管理事务边界。这种类型的事务管理需要更多的编码工作,但是提供了更精确的控制,允许你在事务管理中进行更多的定制。

2.1.1 适用场景

编程式事务管理最适用于那些需要进行细粒度事务控制的场合,比如说在一些复杂的业务逻辑中,一个事务中可能需要执行多个操作,而这些操作可能需要不同的事务属性。

2.1.2 实现方法

在Spring中,编程式事务管理可以通过TransactionTemplate或者PlatformTransactionManager接口来实现。

  • 使用TransactionTemplateTransactionTemplate是一个模板类,提供了一种回调机制,允许你在一个事务中执行多个操作。
代码语言:java复制
    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    public MyService(PlatformTransactionManager transactionManager, MyRepository myRepository) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.myRepository = myRepository;

        // 定制事务属性
        this.transactionTemplate.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
        this.transactionTemplate.setIsolationLevel(Isolation.DEFAULT.value());
        this.transactionTemplate.setTimeout(30); // 设置超时时间,单位为秒
    }

    // 编程式事务管理实现
    public void createEntityByTemplate(MyEntity myEntity) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                myRepository.save(myEntity);
            }
        });
    }
  • 使用PlatformTransactionManager:这个接口提供了更低层次的事务控制,允许你显式地开始、提交和回滚事务。
代码语言:java复制
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createEntityByTransactionManager(MyEntity myEntity) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            myRepository.save(myEntity);
            transactionManager.commit(status);
        } catch (DataAccessException e) {
            transactionManager.rollback(status);
            throw e;
        }
    }

2.2 声明式事务管理

声明式事务管理允许你在配置中声明事务边界,而不是在代码中。这种方式减少了样板代码的数量,让业务逻辑更加清晰,并且在大多数情况下,是更推荐使用的事务管理策略。

2.2.1 适用场景

声明式事务管理通常用于那些事务边界清晰、事务属性统一的场合,如服务层的方法,特别是在大多数事务只需要基本的CRUD操作的场合。

2.2.2 实现方法

在Spring中,声明式事务管理通常通过@Transactional注解来实现。

  • 使用@Transactional注解:你可以在类或方法上使用这个注解来声明事务边界和属性。
代码语言:java复制
    @Transactional(transactionManager = "transactionManagerOne",propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 30)
    public void createEntity(MyEntity myEntity) {
        myRepository.save(myEntity);
    }

三、Spring事务的深度剖析

本章,我们将深度探索Spring事务的源码,并通过Debug关键代码位置来揭示Spring事务的工作原理。我们也会详细演示如何在复杂的业务场景中实际使用Spring事务。

3.1 Spring事务的工作原理

Spring事务的核心是AOP(Aspect-Oriented Programming,面向切面编程)和代理模式,通过这些核心概念和机制,我们可以理解Spring事务是如何工作的。

3.1.1 AOP与代理模式

Spring事务管理利用AOP代理模式,为目标对象创建代理对象,以实现事务的开启、提交、回滚等。

3.1.1.1 动态代理与CGLIB代理
  • 动态代理:如果目标对象实现了接口,Spring会用JDK的动态代理来创建代理对象。
代码语言:java复制
// 示例:使用Java Reflection API中的Proxy.newProxyInstance方法来创建代理对象
MyService proxy = (MyService) Proxy.newProxyInstance(
    MyServiceImpl.class.getClassLoader(),
    MyServiceImpl.class.getInterfaces(),
    new MyInvocationHandler(new MyServiceImpl()));
  • CGLIB代理:如果目标对象没有实现接口,Spring会用CGLIB来创建代理对象。
代码语言:java复制
// 示例:使用CGLIB的Enhancer类来创建目标类的子类作为代理对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyServiceImpl.class);
enhancer.setCallback(new MethodInterceptorImpl());
MyServiceImpl proxy = (MyServiceImpl) enhancer.create();

3.1.2 事务管理器PlatformTransactionManager

PlatformTransactionManager是Spring事务管理的核心,它定义了事务的基本操作。

代码语言:java复制
// 示例:获取事务状态并执行事务
DefaultTransactionStatus status = (DefaultTransactionStatus) transactionManager.getTransaction(new DefaultTransactionDefinition());

try {
  // 执行业务逻辑
  // ...
  // 提交事务
  transactionManager.commit(status);
} catch (Exception ex) {
  // 发生异常,回滚事务
  transactionManager.rollback(status);
  throw ex;
}

3.1.3 事务同步与TransactionSynchronizationManager

TransactionSynchronizationManager管理事务同步状态。

代码语言:java复制
// 示例:注册事务同步
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
  @Override
  public void afterCommit() {
    // 在事务提交后执行的逻辑
  }
});

3.2 复杂业务场景下的事务管理

在复杂的业务场景下,详细理解和精确应用事务管理是至关重要的。

3.2.1 嵌套事务

嵌套事务中,主事务包含多个子事务,每个子事务都可以单独提交和回滚。如果主事务失败,所有子事务都会回滚。

代码语言:java复制
// 示例:使用NESTED传播属性创建嵌套事务
@Transactional(propagation = Propagation.NESTED)
public void nestedTransactionMethod() {
  // 执行业务逻辑
}

3.2.2 分布式事务

在微服务架构中,分布式事务协调多个服务,确保事务的一致性。

代码语言:java复制
// 示例:使用两阶段提交(2PC)来协调分布式事务
// Service 1
@Transactional
public void service1Method() {
  // 执行业务逻辑
  // ...
  // 第一阶段:预提交
  // 第二阶段:提交
}

// Service 2
@Transactional
public void service2Method() {
  // 执行业务逻辑
  // ...
  // 第一阶段:预提交
  // 第二阶段:提交
}

3.2.3 长事务

长事务涉及大量数据处理和复杂业务逻辑,应特别注意事务的隔离级别、锁策略和性能问题。

代码语言:java复制
// 示例:处理长事务
@Transactional(isolation = Isolation.SERIALIZABLE, timeout = 1200)
public void longTransactionMethod() {
  // 执行业务逻辑
  // ...
}

四、Spring事务的属性配置与策略

本章将专注于详细介绍Spring事务的各种属性配置和策略,以及这些配置和策略如何影响事务的行为。

4.1 事务属性的配置

Spring事务的属性包括隔离级别、传播行为、只读标志、超时设置等。通过这些属性的组合,我们可以为不同的业务场景配置合适的事务策略。

4.1.1 隔离级别(Isolation Level)

隔离级别定义了一个事务可能会受到其他并发事务的哪些影响。Spring提供了与大多数数据库一致的隔离级别:

  • DEFAULT:使用数据库默认的隔离级别。
  • READ_UNCOMMITTED:允许读取未提交的数据。
  • READ_COMMITTED:只允许读取已提交的数据。
  • REPEATABLE_READ:确保多次读取的结果是一致的。
  • SERIALIZABLE:提供严格的事务隔离,以避免幻读。
代码语言:java复制
// 示例:配置事务隔离级别
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transactionalMethod() {
  // 执行业务逻辑
}

4.1.2 传播行为(Propagation Behavior)

传播行为定义了事务的边界。Spring定义了7种传播行为:

  • REQUIRED:支持当前事务,如果没有事务则创建新事务。
  • SUPPORTS:支持当前事务,如果没有事务则以非事务方式执行。
  • MANDATORY:支持当前事务,如果没有事务则抛出异常。
  • REQUIRES_NEW:创建新事务,如果有当前事务,则将当前事务挂起。
  • NOT_SUPPORTED:以非事务方式执行,如果有当前事务,则将当前事务挂起。
  • NEVER:以非事务方式执行,如果存在事务则抛出异常。
  • NESTED:如果当前存在事务,则执行一个嵌套事务,如果当前没有事务,则等同于REQUIRED。
代码语言:java复制
// 示例:配置事务传播行为
@Transactional(propagation = Propagation.REQUIRED)
public void transactionalMethod() {
  // 执行业务逻辑
}

4.1.3 只读标志(Read-Only Flag)

只读标志可以帮助数据库优化事务,如果所有的数据操作都是读取操作,则可以将事务标记为只读。

代码语言:java复制
// 示例:配置只读事务
@Transactional(readOnly = true)
public void transactionalMethod() {
  // 执行业务逻辑
}

4.1.4 超时设置(Timeout)

超时设置定义了事务的最长运行时间,如果超出这个时间,则事务会被回滚。

代码语言:java复制
// 示例:配置事务超时时间
@Transactional(timeout = 300)
public void transactionalMethod() {
  // 执行业务逻辑
}

4.2 事务策略的应用

正确地配置和应用事务属性是确保应用程序稳定性和数据一致性的关键。我们需要根据业务逻辑的特性,以及性能和一致性的需求,来选择合适的事务属性。

4.2.1 高并发场景

在高并发场景下,应该优先考虑使用较低的隔离级别和合适的超时设置,以减少锁竞争和提高系统的吞吐量。

代码语言:java复制
// 示例:配置适合高并发场景的事务属性
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 100)
public void highConcurrencyMethod() {
  // 执行业务逻辑
}

4.2.2 数据一致性要求高的场景

如果数据一致性是首要考虑的因素,我们应该选择较高的隔离级别,以防止脏读、不可重复读和幻读。

代码语言:java复制
// 示例:配置适合数据一致性要求高场景的事务属性
@Transactional(isolation = Isolation.SERIALIZABLE)
public void highConsistencyMethod() {
  // 执行业务逻辑
}

五、Spring事务的实践与复杂业务场景

在本章,我们将着重探讨如何在实际项目中应用Spring事务,以及如何在复杂的业务场景下管理事务,以确保数据的一致性和完整性。

5.1 实践中的Spring事务管理

在实际项目中使用Spring事务时,开发人员应该深入了解业务逻辑并合理配置事务属性,以满足业务需求并确保系统的稳定性和性能。

5.1.1 选择合适的事务边界

事务的边界选择至关重要。一般而言,事务应该尽可能的小并且简短,避免长事务占用系统资源。

代码语言:java复制
// 示例:选择合适的事务边界
@Transactional
public void serviceMethod() {
  // 事务开始
  try {
    // 执行核心业务逻辑
    coreBusinessLogic();
  } catch (Exception e) {
    // 业务异常处理
    handleBusinessException(e);
  }
  // 事务结束
}

5.1.2 合理配置事务属性

根据业务的实际需求和性质,合理配置事务的隔离级别、传播行为、超时时间和只读标志。

代码语言:java复制
// 示例:合理配置事务属性
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, timeout = 200)
public void serviceMethod() {
  // 执行业务逻辑
}

5.2 复杂业务场景下的事务管理

在复杂的业务场景下,如何合理的管理事务变得尤为重要,下面我们通过一些实际案例来演示如何更加灵活和高效的使用Spring事务。

5.2.1 分布式事务

在微服务架构下,一个业务操作可能涉及到多个服务的协同工作,这就涉及到了分布式事务的问题。

方案1:基于2PC的分布式事务

2PC(两阶段提交)是一种经典的分布式事务解决方案,其主要包括准备阶段和提交阶段。每个参与的服务都需要实现相应的准备和提交逻辑,如下示例:

代码语言:java复制
// 示例:2PC分布式事务实现
public class DistributedTransactionService {

  @Transactional
  public void prepare() {
    // 准备阶段的业务逻辑
  }

  @Transactional
  public void commit() {
    // 提交阶段的业务逻辑
  }
}
方案2:基于Saga的分布式事务

Saga模式是一种更为轻量级和灵活的分布式事务解决方案。每个参与的服务只需定义自己的业务逻辑和补偿逻辑。例如,支付服务可以定义扣款逻辑和退款逻辑。

代码语言:java复制
// 示例:Saga分布式事务实现
public class PaymentService {

  @Transactional
  public void debit() {
    // 扣款逻辑
  }

  @Transactional
  public void refund() {
    // 退款逻辑
  }
}

5.2.2 长事务与延迟确认

在某些场景下,可能需要处理长事务和延迟确认的问题,例如,用户下单后需要在一定时间内支付。

解决方案:采用状态机和延迟队列

可以使用状态机来管理业务对象的状态变迁,并利用延迟队列来实现延迟确认的业务逻辑。

代码语言:java复制
// 示例:状态机与延迟队列实现长事务和延迟确认
public class OrderService {

  @Transactional
  public void createOrder() {
    // 创建订单,并将订单状态设为待支付
  }

  @Transactional
  public void confirmPayment() {
    // 收到支付确认后,将订单状态设为已支付
  }
}

六、Spring事务失效及其解决方案

Spring事务失效问题可能会导致不一致的数据状态和其他不可预见的后果。为此,本章将展现详细的、完整的并可执行的代码示例来解决常见的事务失效问题。

6.1 事务失效常见原因及解决方案

Spring事务失效可能由多种原因导致,下面将列举出主要原因,并附上完整的、可执行的代码示例和解决方案。

6.1.1 非public方法上使用@Transactional
问题描述

如果在protected、private或包级私有的方法上使用@Transactional注解,Spring将无法代理这些方法,导致事务失效。

解决方案

确保@Transactional注解仅用在public方法上。

示例代码
代码语言:java复制
@Service
public class TransactionalService {
    
    @Autowired
    private MyRepository myRepository;
    
    // 正确:@Transactional注解在public方法上
    @Transactional
    public void correctTransactionalMethod() {
        myRepository.save(new MyEntity());
    }
    
    // 错误:@Transactional注解在protected方法上,事务将不会生效
    @Transactional
    protected void incorrectTransactionalMethod() {
        myRepository.save(new MyEntity());
    }
}
6.1.2 自调用导致事务失效
问题描述

由于Spring AOP的代理机制,如果一个对象内部的非事务方法调用事务方法,事务将失效。

解决方案

将事务方法移到另一个Bean中,或使用AopContext.currentProxy()来调用当前Bean的事务方法。

示例代码
代码语言:java复制
@Service
public class SelfInvocationService {

    @Autowired
    private TransactionalService transactionalService;

    public void method() {
        // 正确:非事务方法调用另一个Bean的事务方法
        transactionalService.correctTransactionalMethod();
    }
    
    // 这个方法为事务方法,如果被上面的非事务方法直接调用,事务会失效
    @Transactional
    public void anotherTransactionalMethod() {
        // ... your business logic ...
    }
}
6.1.3 数据库或数据库引擎不支持事务
问题描述

如果所使用的数据库或数据库引擎不支持ACID事务,Spring的事务管理将无法正常工作。

解决方案

选择并配置支持ACID事务的数据库和数据库引擎。

示例代码
代码语言:yaml复制
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=UTC
    username: myuser
    password: mypass
代码语言:java复制
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(
                "jdbc:mysql://localhost:3306/mydb",
                "myuser",
                "mypass"
        );
    }
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

总结

至此,本篇结束。本文全面深入地探讨了Spring事务的原理、管理类型、深度剖析以及在复杂业务场景中的应用。通过对Spring事务的深度剖析和实际应用案例的探讨,我们可以更加清晰、准确地理解Spring事务在实际开发中的运用,更加灵活地处理各种复杂的业务场景。

在实际开发过程中,精确地把握事务边界、合理配置事务属性以及灵活应用分布式事务是至关重要的。这不仅可以确保数据的一致性和完整性,还能优化系统性能,提高系统的稳定性和可靠性。


参考文献

  1. 【技术干货】Spring事务原理一探 - 知乎
  2. Spring事务实现原理 - 博客园
  3. Spring事务的实现原理(@Transactional原理 失效场景) - CSDN
  4. Spring笔记(4) - Spring的编程式事务和声明式事务详解 - 博客园
  5. spring-framework-data-access - Spring官方
  6. 数据库事务的ACID和Spring事务传播 - 微信公众号
  7. 美团二面:spring事务不生效的15种场景 - 微信公众号
  8. Spring事务的这10种坑,你稍不注意可能就会踩中! - 微信公众号

0 人点赞