背景

本文是《Java 后端从小白到大神》修仙系列之框架学习,Java框架之Spring框架第三篇。本篇文章主要聊Java框架,那么必然从Spring框架开始,可以说Spring框架是Java企业级开发的基石,若想详细学习请点击首篇博文,我们现在开始吧。

文章概览

  1. 数据访问与事务管理(Data Access & Tx)

Spring框架

数据访问与事务管理(Data Access & Tx)

1. 数据访问与事务管理

Spring 给我们提供了强大的数据访问机制(如 JdbcTemplate、ORM 集成、MyBatis 支持)和灵活的事务控制(包括声明式事务、编程式事务、事务传播行为、隔离级别、以及多数据源与分布式事务等),极大地简化了企业级应用的数据库操作和事务管理。

下图简要概述了 Spring 如何从数据库连接开始,层层递进地提供数据访问和对象映射能力:

graph TD
    A[应用代码] --> B{Spring 数据访问层}
    
    B --> C[DataSource - 数据源]
    C -- 实现 --> D[DriverManagerDataSource]
    C -- 实现 --> E[连接池: HikariCP / Druid]
    D --> F[数据库]
    E --> F

    B --> G[JdbcTemplate]
    G -- 依赖 --> C
    G -- 操作SQL --> F

    B --> H[ORM 集成: JPA/Hibernate]
    H -- 依赖 --> C
    H -- 映射Java对象 <--> 数据库表 --> F
    H -- JPA标准实现 --> I[Hibernate]

    B --> J[MyBatis 支持]
    J -- 依赖 --> C
    J -- Mapper接口 + XML/注解SQL --> F
    J -- 核心会话 --> K[SqlSession]
    K -- Spring管理 --> L[SqlSessionTemplate]
    L --> J

    subgraph 核心流程
        C --> G
        C --> H
        C --> J
    end

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#dff,stroke:#333,stroke-width:2px
    style G fill:#fbf,stroke:#333,stroke-width:2px
    style H fill:#fdf,stroke:#333,stroke-width:2px
    style J fill:#efe,stroke:#333,stroke-width:2px

2. JdbcTemplate:简化 JDBC 操作

  • 概念:Spring JDBC 提供的 JdbcTemplate 是对原生 JDBC API 的高度封装,旨在简化样板代码自动管理资源(如数据库连接的获取与关闭)、并提供统一的异常翻译,从而大幅提升开发效率和代码可维护性。
  • 配置:在使用 JdbcTemplate 前,你需要向 Spring 容器注入(Dependency Injection)一个 DataSource BeanDataSource 是一个标准的 Java 接口(javax.sql.DataSource),它定义了获取数据库连接的方法。
    • 实现方式
      • DriverManagerDataSource:这是 Spring 框架提供的一个最简单的 DataSource 实现,每次需要数据库连接时,它都会直接创建一个新的 JDBC 连接。它不具备连接池功能,通常只用于开发和测试环境,不适合高并发的生产环境。
      • 连接池(如 HikariCP、Druid、Tomcat JDBC Connection Pool 等):这些是更高级、性能更优的 DataSource 实现。它们会维护一个数据库连接的“池子”。当应用程序需要连接时,它们从池中获取一个已存在且空闲的连接;当连接使用完毕后,连接不会被关闭,而是放回池中复用。这显著提高了连接效率和系统吞吐量,是生产环境的首选。
  • 统一的异常封装:Spring 将底层 JDBC 抛出的、数据库厂商特定的 SQLException 捕获,并将其**翻译(Translate)**成 Spring 自己的、统一且更具语义化的 DataAccessException 异常体系及其子类(例如 DuplicateKeyException 表示主键重复,DataIntegrityViolationException 表示数据完整性违规)。这使得你的错误处理代码更清晰,不依赖于具体的数据库厂商,提升了代码的可移植性。

配置示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
public class DataSourceConfig {
    @Bean // 定义一个DataSource Bean
    public DataSource dataSource() {
        // 使用 DriverManagerDataSource 作为示例,实际生产环境应使用连接池
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/testdb");
        ds.setUsername("root");
        ds.setPassword("password");
        return ds;
    }

    @Bean // JdbcTemplate 会自动注入上面定义的 DataSource Bean
    public JdbcTemplate jdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

常用 API

  • jdbcTemplate.update(sql, args...):执行 INSERT/UPDATE/DELETE 操作。
  • jdbcTemplate.queryForObject(sql, rowMapper, args...):查询并返回单行结果。
  • jdbcTemplate.query(sql, rowMapper, args...):查询并返回多行结果(列表)。
  • jdbcTemplate.batchUpdate(sql, batchArgs):执行批量更新。
  • jdbcTemplate.queryForList(sql, elementType):返回简单类型(如 String, Integer)列表。
  • 命名参数支持:对于复杂的 SQL 语句,Spring 还提供了 NamedParameterJdbcTemplate,允许使用命名参数(例如 :username)代替传统的问号占位符 ?,大大提高了 SQL 语句的可读性和维护性。
示例 DAO
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Repository
public class UserDao {
    private final JdbcTemplate jdbc;

    public UserDao(JdbcTemplate jdbc) { this.jdbc = jdbc; }

    public int save(User u) {
        return jdbc.update(
            "INSERT INTO users(username, registered_at) VALUES(?, ?)",
            u.getUsername(), u.getRegisteredAt());
    }

    public User findById(Long id) {
        return jdbc.queryForObject(
            "SELECT id, username, registered_at FROM users WHERE id = ?",
            (rs, rn) -> new User(
                rs.getLong("id"), rs.getString("username"), rs.getTimestamp("registered_at").toLocalDateTime()),
            id);
    }

    public List<User> findAll() {
        return jdbc.query(
            "SELECT id, username, registered_at FROM users",
            (rs, rn) -> new User(
                rs.getLong("id"), rs.getString("username"), rs.getTimestamp("registered_at").toLocalDateTime()));
    }
}

3. ORM 集成(Hibernate / JPA):对象与关系的映射

  • 概念
    • JPA(Java Persistence API):它是一个 Java 标准/规范,定义了对象关系映射(ORM)的 API。它规定了如何使用注解将 Java 对象映射到数据库表,以及如何通过 EntityManager 来执行数据库操作。
    • Hibernate:它是 JPA 规范的一个具体实现。当你配置使用 JPA 时,底层通常会选择一个实现,Hibernate 是其中最流行和功能最强大的。
    • Spring 通过 LocalContainerEntityManagerFactoryBean(用于 JPA)或 LocalSessionFactoryBean(用于原生 Hibernate)将 ORM 框架深度集成到其容器中。
  • 配置:在 Spring 应用中集成 ORM,你需要进行以下配置,告诉 Spring 如何设置 ORM 环境:
    • 数据源:与 JdbcTemplate 配置类似,ORM 也需要一个 DataSource Bean 来获取数据库连接。
    • 实体扫描packagesToScan 属性指定了 Spring 应该去哪个包下扫描带有 @Entity 注解的实体类,以便 ORM 框架能够建立 Java 对象与数据库表之间的映射关系。
    • 方言、DDL 自动化等属性:这些是 ORM 框架(如 Hibernate)特有的配置,例如 hibernate.dialect 用于告诉 Hibernate 当前使用的数据库类型,以便它生成正确的 SQL 语句;hibernate.hbm2ddl.auto 用于控制启动时数据库表结构的自动更新策略。

配置示例:JPA 和 Spring 事务管理器

这段代码定义了两个核心的 Spring Bean,用于初始化 JPA/Hibernate 环境,并将其与 Spring 的事务管理体系整合:

  1. LocalContainerEntityManagerFactoryBean (emf Bean):

    • 作用:这是一个 Spring 提供的工厂 Bean,负责创建 JPA 的核心接口 EntityManagerFactory。它就像是 JPA 的“会话工厂”,负责创建 EntityManager 实例(EntityManager 是用于实际数据库操作的接口)。
    • 关键配置:包括设置数据源、扫描实体类所在的包、指定 JPA 供应商(这里是 Hibernate)以及配置 Hibernate 的具体属性。
  2. JpaTransactionManager (transactionManager Bean):

    • 作用:这是 Spring 事务管理的核心接口 PlatformTransactionManager 的一个 JPA 实现。它负责将 Spring 的事务管理(特别是 @Transactional 注解)与底层的 JPA 事务进行集成,确保你的数据库操作能够正确地开启、提交或回滚事务。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableTransactionManagement // 启用Spring的声明式事务管理
public class JpaConfig {
    @Bean
    public LocalContainerEntityManagerFactoryBean emf(DataSource ds) {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(ds); // 指定数据源
        emf.setPackagesToScan("com.example.domain"); // 扫描实体类所在的包
        JpaVendorAdapter vendor = new HibernateJpaVendorAdapter(); // 指定JPA供应商为Hibernate
        emf.setJpaVendorAdapter(vendor);
        Properties props = new Properties();
        props.put("hibernate.hbm2ddl.auto", "update"); // 启动时自动更新数据库表结构
        props.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); // 指定MySQL数据库方言
        emf.setJpaProperties(props);
        return emf;
    }

    @Bean // 将JPA事务管理器注册为Spring Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}
  • 使用:在完成上述配置后,你就可以开始定义你的数据模型和数据操作接口了:
    • 实体类:在普通的 Java 类上使用 @Entity@Table@Id@GeneratedValue 等 JPA 注解来标注,将它们映射到数据库表。通过这些实体类,你可以在 Java 代码中面向对象地操作数据库数据
    • Repository 接口:你可以通过两种主要方式进行数据操作:
      • 继承 JpaRepository (Spring Data JPA 推荐):这是最推荐和最简化的方式。通过继承 Spring Data JPA 提供的 JpaRepository 接口,你可以自动获得大量的基本 CRUD(创建、读取、更新、删除)操作方法,而无需编写任何实现代码。
      • 手写 @Repository 并注入 EntityManager:如果你需要更细粒度的控制或执行复杂的 JPA 操作,也可以自定义一个 DAO 类,用 @Repository 标注,并注入 EntityManager 实例来直接进行数据库交互。

示例:Spring Data JPA(定义实体和 Repository)

这段代码展示了如何利用 Spring Data JPA 来定义数据模型和数据访问接口,极大地简化了 DAO 层开发:

  1. Product 实体类

    • 作用:这是一个普通的 Java 对象,但由于使用了 @Entity@Table 等 JPA 注解,它被映射到了数据库中的 products 表。当你操作 Product 对象时,ORM 框架会在底层将其转换为 SQL 操作数据库。
  2. ProductRepo 接口

    • 作用:通过继承 JpaRepository<Product, Long>,Spring Data JPA 会在运行时自动为你生成针对 Product 实体的各种 CRUD 方法的实现。你无需手动编写 save(), findById(), findAll() 等方法的代码。
    • findByNameContaining(String keyword) 方法则演示了 Spring Data JPA 的“方法名解析查询”能力:Spring 会根据方法名自动生成对应的 SQL 查询语句(例如 SELECT * FROM products WHERE name LIKE '%keyword%'),进一步减少了 SQL 编写量。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Entity // 标记这是一个JPA实体类
@Table(name = "products") // 映射到数据库中的products表
public class Product {
    @Id // 标记为主键
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键生成策略,IDENTITY表示自增长
    private Long id;
    private String name;
    // getters/setters 略
}

@Repository // 标记为一个Spring Repository组件
public interface ProductRepo extends JpaRepository<Product, Long> { // 继承JpaRepository,自动获得CRUD功能
    // Spring Data JPA 会根据方法名自动生成SQL查询
    List<Product> findByNameContaining(String keyword);
}

4. SQLSession & MyBatis 支持

MyBatis 是一个优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。在 Spring 应用中,MyBatis 能够深度集成,提供灵活且强大的数据访问能力。

4.1 SqlSession 核心概念

在深入了解 MyBatis 与 Spring 的集成之前,我们首先要理解 MyBatis 中的一个核心概念:SqlSession

  • 什么是 SqlSession SqlSession 是 MyBatis 对外暴露的主要操作接口,它代表了 MyBatis 与数据库之间的一次会话(Session)。你可以把它想象成 MyBatis 版的 JDBC Connection,但它提供了更高层次的抽象和更丰富的功能。
  • SqlSession 的作用
    • 执行 SQL 命令的门户:它是你执行 SQL 语句(无论是查询、插入、更新还是删除)的入口。当你调用 Mapper 接口中的方法时,MyBatis 内部就是通过 SqlSession 实例来完成与数据库的实际交互。
    • 事务管理SqlSession 可以用于手动管理事务(例如调用 commit()rollback())。但在 Spring 环境中,通常是由 Spring 的事务管理器来统一管理 SqlSession 的事务生命周期,让你能够通过简单的 @Transactional 注解来实现声明式事务。
    • 一级缓存管理:每个 SqlSession 都拥有自己独立的一级缓存。在同一个 SqlSession 的生命周期内,如果多次查询相同的数据,MyBatis 会直接从一级缓存中获取,从而避免不必要的数据库访问,提高性能。
  • 生命周期SqlSession非线程安全的,而且是短生命周期的对象。最佳实践是将其视为一次请求或一个业务操作的“工作单元”,在每次数据库操作时被创建,并在操作完成后立即关闭。

4.2 MyBatis 与 Spring 的集成关键点

在 Spring 框架中,MyBatis 与 Spring 深度集成,通常不会直接手动创建和管理 SqlSession。这是因为 Spring 提供了一系列集成组件,简化了 SqlSession 的管理,使其与 Spring 的 IoC 容器和事务管理无缝协作。

  • SqlSessionFactorySqlSessionTemplate
    • SqlSessionFactory:类似于 JPA 中的 EntityManagerFactory,它是创建 SqlSession 实例的工厂。它是线程安全的,通常一个应用只需要一个实例。
    • SqlSessionTemplate:这是 Spring 集成 MyBatis 的核心。它是线程安全的,并且可以被安全地注入到你的 DAO 或 Service 中。SqlSessionTemplate 会智能地管理 SqlSession 的生命周期,确保每个线程拥有独立的 SqlSession 实例,并在操作完成后正确关闭。同时,它会自动参与到 Spring 的事务管理中,使得 SqlSession 的操作能够被 Spring 的 @Transactional 注解所控制。
    • 在 Spring Boot 项目中,MyBatis 的集成非常简单,只需添加相应依赖,Spring Boot 会自动配置好 SqlSessionFactorySqlSessionTemplate,大大简化了配置。在非 Spring Boot 环境下,你可能需要手动配置 SqlSessionFactoryBean 来创建 SqlSessionFactory,以及 SqlSessionTemplate Bean。
  • Mapper 接口 + XML 映射:MyBatis 允许你将 SQL 语句写在独立的 XML 映射文件中,并通过 Mapper 接口与这些 SQL 语句进行绑定。这种方式提供了极大的灵活性,允许你编写复杂的、高度定制化的 SQL。
  • 支持分页插件、缓存等功能:MyBatis 生态系统提供了丰富的插件机制,例如著名的 PageHelper 用于实现方便的分页查询,以及自带的二级缓存功能(与 SqlSession 的一级缓存不同,二级缓存是跨 SqlSession 共享的,通常需要额外配置和开启)。

示例代码

1
2
3
4
5
@Mapper // 标记为一个MyBatis Mapper接口,Spring会为其创建代理,并注入到Spring容器中
public interface UserMapper {
    @Select("SELECT * FROM users WHERE id = #{id}") // 直接在方法上编写SQL语句
    User findById(Long id);
}

4.3 Spring Data 简介

概述:Spring Data 是一个 umbrella project(伞形项目),旨在提供统一的数据访问抽象,并简化不同数据存储(如关系型数据库、NoSQL 数据库、云数据服务等)的访问。其主要子项目有 Spring Data JPA、Spring Data MongoDB、Spring Data Redis 等,专注于简化 DAO 层开发。

  • Repository 接口自动实现:通过继承 Spring Data 提供的 Repository 接口,开发者无需编写 DAO 实现类,Spring Data 会在运行时自动生成实现。
  • 方法命名自动派生 SQL:Spring Data JPA 尤其擅长根据 Repository 接口中定义的方法名(如 findByUsernameAndEmail)自动生成对应的 SQL 查询。
  • 自定义查询:支持通过 @Query 注解编写 JPQL 或原生 SQL,或者使用 QueryDSL 进行类型安全的查询。

示例代码

1
2
3
4
5
6
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatus(String status); // 根据方法名自动生成查询

    @Query("SELECT o FROM Order o WHERE o.total > ?1") // 使用@Query注解自定义JPQL查询
    List<Order> findExpensiveOrders(BigDecimal minTotal);
}

5. 异常处理与统一封装:更友好的数据库错误处理

概述:Spring 致力于提供一套统一且可移植的数据访问异常体系,以解决底层数据库异常的厂商相关性问题。

  • DataAccessException 统一异常体系:Spring 会捕获底层数据库(如 JDBC 的 SQLException)或 ORM 框架抛出的具体异常,并将其翻译成 org.springframework.dao.DataAccessException 及其子类。这个异常体系是非检查异常(Unchecked Exception),这意味着你通常不需要强制 try-catch
  • 不再暴露 JDBC 原生异常:这种机制使得业务代码无需关心底层数据库的具体实现细节(如不同的错误码),只需处理 Spring 统一的 DataAccessException 即可。

示例代码

1
2
3
4
5
6
7
try {
    jdbcTemplate.update("INSERT INTO users(name) VALUES(?)", "Tom");
} catch (DataAccessException e) { // 统一捕获 Spring 提供的 DataAccessException
    // 在这里根据 e 的具体子类进行业务判断和处理
    // 例如:if (e instanceof DuplicateKeyException) { /* 处理重复数据 */ }
    logger.error("Database error: " + e.getMessage());
}

6. 事务管理:保证数据操作的原子性

  • 概念:事务是数据库操作中不可或缺的一部分,它确保一组操作要么全部成功,要么全部失败(原子性)。事务具备 ACID 特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。Spring 提供统一抽象 PlatformTransactionManager,支持多种事务管理器(如 DataSourceTransactionManagerJpaTransactionManagerHibernateTransactionManagerJtaTransactionManager),并支持声明式和编程式事务管理。

6.1 声明式事务管理:更简洁、面向注解

  • 方式:这是 Spring 事务管理最常用和推荐的方式。通过在配置类上添加 @EnableTransactionManagement 注解,并在 Service 层的方法或类上添加 @Transactional 注解,Spring 就能自动地为这些方法创建 AOP 代理,在方法执行前开启事务,方法成功执行后提交事务,发生异常时回滚事务,无需手动编写事务管理代码。
  • 底层实现:声明式事务的强大之处在于它依赖于 Spring 的 AOP (面向切面编程) 机制@EnableTransactionManagement 会激活 Spring 容器,使其能够识别 @Transactional 注解,并为被 @Transactional 标注的 Bean 自动创建代理对象(通常是 JDK 动态代理或 CGLIB 代理)。当客户端调用被代理的方法时,实际上是代理对象拦截了调用,并在方法执行前后织入(weave)了事务管理逻辑。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class OrderService {
    // 假设 orderRepo 和 auditRepo 都是注入的 Spring Data JPA Repository
    @Autowired 
    private OrderRepo orderRepo;
    @Autowired 
    private AuditRepo auditRepo;

    @Transactional( // 声明式事务注解
        propagation = Propagation.REQUIRED, // 传播行为:如果当前没有事务,就新建一个;如果有,就加入。
        isolation = Isolation.READ_COMMITTED, // 隔离级别:读取已提交的数据,避免脏读。
        rollbackFor = Exception.class, // 遇到任何Exception都回滚(包括受检异常)。
        readOnly = false // 事务是可读写的。
    )
    public void placeOrder(Order order) {
        orderRepo.save(order); // 保存订单
        auditRepo.log(order);  // 记录审计日志
        // 如果这里或后续操作抛出任何 Exception 类型的异常,整个 placeOrder 方法的操作都将回滚。
        // 例如:
        // if (order.getPrice().compareTo(BigDecimal.valueOf(1000)) > 0) {
        //     throw new RuntimeException("订单金额过大,不允许下单!");
        // }
    }
}
  • 事务属性详解
    • 传播行为(propagation:定义了当一个方法在一个事务中调用另一个方法时,事务应该如何传播。常用的有:
      • Propagation.REQUIRED (默认):如果当前没有事务,就新建一个;如果当前存在事务,就加入到这个事务中。
      • Propagation.REQUIRES_NEW:总是新建一个事务,如果当前存在事务,就将当前事务挂起。
      • Propagation.NESTED:如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则新建一个事务。
    • 隔离级别(isolation:定义了事务之间的隔离程度,以解决并发事务可能导致的问题:
      • Isolation.READ_UNCOMMITTED:最低级别,可能发生脏读、不可重复读、幻读。
      • Isolation.READ_COMMITTED (大多数数据库默认):避免脏读,可能发生不可重复读、幻读。
      • Isolation.REPEATABLE_READ (MySQL 默认):避免脏读、不可重复读,可能发生幻读。
      • Isolation.SERIALIZABLE:最高级别,避免所有并发问题,但性能最低。
    • 只读(readOnlytrue 表示事务是只读的。对于查询操作,可以设置为 true,有助于底层数据库进行性能优化,但如果在此事务中执行了写入操作,则会抛出异常。
    • 回滚策略(rollbackFor, noRollbackFor:控制哪些异常会触发事务回滚。
      • Spring 默认回滚规则:Spring 事务默认只对运行时异常(RuntimeException 及其子类)和 Error 进行回滚。对于受检异常(Checked Exception,即非 RuntimeExceptionException 子类,例如 IOException,默认不触发回滚。
      • rollbackFor = {Exception.class}:明确指定遇到 Exception 类或其任何子类都回滚。
      • noRollbackFor = {MyCheckedException.class}:明确指定即使遇到 MyCheckedException 也不回滚。

测试事务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {DataSourceConfig.class, JpaConfig.class, OrderService.class, OrderRepo.class, AuditRepo.class})
public class TransactionTest {
    @Autowired
    private OrderService service;

    @Test
    void testRollback() {
        // 断言当传入一个会导致业务异常的订单时,placeOrder方法会抛出RuntimeException
        assertThrows(RuntimeException.class, () -> {
            service.placeOrder(invalidOrder()); // 假设 invalidOrder() 会触发 placeOrder 内部的异常
        });
        // 验证 orderRepo 和 auditRepo 均未提交数据,因为事务已回滚
        // 例如:通过查询数据库确认数据不存在
    }
}

6.2 编程式事务管理:手动控制事务边界

  • 方式:编程式事务管理需要你手动编写代码来控制事务的开始、提交和回滚。虽然不如声明式事务常用,但在某些特殊场景(例如事务边界需要非常动态地控制,或在非 Spring 管理的线程中执行事务)下仍然有用。
  • 主要方式
    • 使用 PlatformTransactionManager 接口:直接调用其 getTransaction()commit()rollback() 方法。这种方式代码量较大,且需要手动处理异常。
    • 使用 TransactionTemplate:Spring 推荐的编程式事务方式。它封装了 PlatformTransactionManager 的样板代码和异常处理,提供了更简洁的回调机制。

示例:使用 TransactionTemplate (推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Service
public class AccountService {
    @Autowired
    private AccountDao accountDao; // 假设 AccountDao 负责数据库操作
    
    private final TransactionTemplate transactionTemplate; // Spring推荐的编程式事务封装

    // 通过构造器注入 PlatformTransactionManager 并初始化 TransactionTemplate
    public AccountService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        // 可以设置默认的事务属性,例如:
        // this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
    }

    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // execute 方法接收一个 TransactionCallback 接口,在这个回调中执行事务性操作
        transactionTemplate.execute(status -> { // status 参数可用于手动回滚
            try {
                // 扣钱操作
                accountDao.debit(fromAccountId, amount);
                
                // 模拟一个业务异常,测试回滚
                if (amount.compareTo(new BigDecimal("1000")) > 0) {
                    throw new RuntimeException("转账金额过大,触发回滚");
                }
                
                // 加钱操作
                accountDao.credit(toAccountId, amount);
                
                return "转账成功"; // 返回值可以是你业务操作的结果
            } catch (Exception e) {
                status.setRollbackOnly(); // 捕获到异常时,标记事务为只能回滚
                throw new RuntimeException("转账失败,原因:" + e.getMessage(), e); // 重新抛出异常,以便外部捕获
            }
        });
    }

    // 编程式事务的另一种(更底层)方式:直接使用 PlatformTransactionManager
    public void transferMoneyDirectly(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // 创建事务定义,可以设置传播行为、隔离级别等
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        
        // 获取事务状态
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            accountDao.debit(fromAccountId, amount);
            // ... 其他业务逻辑
            accountDao.credit(toAccountId, amount);
            transactionManager.commit(status); // 提交事务
        } catch (Exception e) {
            transactionManager.rollback(status); // 回滚事务
            throw new RuntimeException("转账失败", e); // 抛出异常
        }
    }
}

6.3 事务同步(Transaction Synchronization)

  • 概念:Spring 允许你注册事务同步回调,这些回调会在事务成功提交或回滚之后执行。它们常用于在事务完成后执行一些资源清理、事件发布、缓存更新、发送通知等操作,确保这些操作与事务的最终结果一致。
  • 使用:通过 TransactionSynchronizationManager.registerSynchronization() 方法注册 TransactionSynchronization 接口的实现。这个接口提供了 afterCommit(), afterCompletion() (无论提交还是回滚), beforeCommit(), beforeCompletion() 等回调方法。

7. 分布式事务与多数据源支持

概述:在复杂的企业级应用中,常常需要操作多个独立的数据源,或者在跨多个服务/数据库的场景下保证操作的原子性,这就涉及到了多数据源和分布式事务。Spring 对此提供了完善的支持,包括管理多个 DataSource 和集成 JTA(Java Transaction API)分布式事务管理器。

下图展示了多数据源和分布式事务的常见场景及解决方案:

graph TD
    A[Spring应用] --> B{多数据源};

    subgraph 单个事务管理器
        B --> C1[DataSource1 - DB1];
        B --> C2[DataSource2 - DB2];
        C1 --> TM1[DataSourceTransactionManager1];
        C2 --> TM2[DataSourceTransactionManager2];
        TM1 --> S1[ServiceMethodA];
        TM2 --> S2[ServiceMethodB];
    end

    subgraph 跨服务/数据库的分布式事务
        D[分布式事务需求];
        D --> E[XA事务 - 两阶段提交];
        E -- 协调者 --> F[JTA事务管理器: Atomikos/Narayana];
        F -- 资源 --> G1[DB1];
        F -- 资源 --> G2[DB2];
        E -- 痛点 --> H[复杂、性能差、阻塞];

        D --> I[柔性事务 - 微服务推荐];
        I --> J[Seata - AT/TCC/SAGA/XA];
        J -- 实现模式 --> K1[AT模式 - 自动补偿];
        J -- 实现模式 --> K2[TCC模式 - Try-Confirm-Cancel];
        J -- 实现模式 --> K3[SAGA模式 - 长事务];
        I -- 特点 --> L[最终一致性、高可用、高性能];
    end

    style A fill:#f9f,stroke:#333,stroke-width:2px;
    style B fill:#bbf,stroke:#333,stroke-width:2px;
    style D fill:#fbd,stroke:#333,stroke-width:2px;
    style E fill:#fdd,stroke:#333,stroke-width:2px;
    style I fill:#dfd,stroke:#333,stroke-width:2px;
    style J fill:#afa,stroke:#333,stroke-width:2px;
  • 多数据源配置:在 Spring 中,你可以配置多个 DataSource Bean,每个 Bean 对应一个独立的数据库连接。通过 @Primary@Qualifier 注解,你可以明确指定哪个数据源是默认的,以及在需要时引用特定的数据源。
  • 事务管理器与数据源绑定:每个 DataSource 可以对应一个 DataSourceTransactionManager。在 Service 层方法上,可以通过 @Transactional(transactionManager = "xxx") 来指定使用哪个数据源的事务管理器。
  • JTA 分布式事务:对于跨多个独立数据库的事务(即 XA 事务),Spring 可以集成 JTA 事务管理器,如 Atomikos、Narayana 等。JTA 负责协调多个资源管理器(如不同的数据库)之间的事务提交或回滚,实现两阶段提交(Two-Phase Commit, 2PC)
  • 微服务推荐方案:传统 XA 分布式事务虽然能保证强一致性,但通常配置复杂、性能较差(因为 2PC 协议的阻塞特性),并且在微服务架构下表现不佳。因此,在微服务场景中,更推荐使用柔性事务方案,如 SeataTCC(Try-Confirm-Cancel)模式
多数据源配置示例(以 Spring Java Config 为例)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
public class MultiDataSourceConfig {

    @Bean // 定义第一个数据源
    @Primary // 标记为主要数据源,没有明确指定时使用它
    public DataSource dataSource1() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setUrl("jdbc:mysql://localhost:3306/db1");
        ds.setUsername("user1");
        ds.setPassword("pass1");
        return ds;
    }

    @Bean // 定义第二个数据源
    public DataSource dataSource2() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setUrl("jdbc:mysql://localhost:3306/db2");
        ds.setUsername("user2");
        ds.setPassword("pass2");
        return ds;
    }

    @Bean // 定义第一个数据源的事务管理器,并标记为Primary
    @Primary
    public PlatformTransactionManager txManager1(@Qualifier("dataSource1") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

    @Bean // 定义第二个数据源的事务管理器
    public PlatformTransactionManager txManager2(@Qualifier("dataSource2") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}
使用注解指定事务管理器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class MultiDbService {

    @Autowired
    private UserRepo1 userRepo1; // 对应db1的数据访问

    @Autowired
    private UserRepo2 userRepo2; // 对应db2的数据访问

    // 使用第一个数据源的事务管理器(txManager1)
    @Transactional(transactionManager = "txManager1")
    public void saveToDb1(User user) {
        userRepo1.save(user);
    }

    // 使用第二个数据源的事务管理器(txManager2)
    @Transactional(transactionManager = "txManager2")
    public void saveToDb2(User user) {
        userRepo2.save(user);
    }

    // 假设需要在一个事务中操作两个数据源,就需要JTA分布式事务管理器
    // @Transactional(transactionManager = "jtaTransactionManager")
    // public void operateAcrossDbs(User user1, User user2) {
    //     userRepo1.save(user1);
    //     userRepo2.save(user2);
    // }
}

分布式事务方案(XA vs Seata 等)

  • 传统 XA 模式

    • 实现:通常借助 Atomikos、Bitronix、Narayana 等 JTA 事务管理器来实现。
    • 原理:依赖底层数据库支持 XA 协议,通过两阶段提交(2PC)协议保证强一致性。
    • 特点:配置复杂、性能相对较差(因为涉及多个协调和锁定阶段),但能保证强一致性。适用于对数据一致性要求极高、跨多个独立数据库的传统单体应用或少数场景。
  • 推荐替代方案(柔性事务):在微服务架构和高并发场景下,为了追求更好的性能和可用性,通常会采用柔性事务方案,牺牲部分隔离性以换取性能和扩展性:

    • Seata:一款开源的分布式事务解决方案,支持 AT(自动事务)、TCC(Try-Confirm-Cancel)、SAGA(长事务)和 XA 模式。它是一个强大的分布式事务协调器,尤其适合微服务架构,能有效解决分布式环境下的数据一致性问题。

    • Seata 支持多种模式,以适应不同的业务场景和对侵入性的要求:

    1. AT 模式(Automatic Transaction)

      • 推荐程度通常是首选模式,因为它对业务代码的侵入性最小。
      • 原理:基于两阶段提交(2PC)的优化,由 Seata 自动进行事务的注册、提交和回滚。它通过解析业务 SQL,自动生成回滚 SQL,并锁定资源,以确保数据一致性。
      • 优点:对业务代码改动量小,易于接入。
      • 缺点:仍涉及一定程度的资源锁定,在极端高并发下可能存在性能瓶颈,且对某些复杂 SQL 或存储过程支持有限。
    2. TCC 模式(Try-Confirm-Cancel)

      • 推荐程度:适用于对一致性要求非常高,且业务逻辑可以清晰划分为 Try、Confirm、Cancel 三个阶段的场景。
      • 原理:需要业务方手动实现 Try(资源预留)、Confirm(确认提交)、Cancel(回滚补偿)三个阶段的逻辑。
      • 优点:不依赖于数据库的事务,性能通常更高,且没有资源长期锁定。
      • 缺点:对业务代码侵入性最强,开发成本较高。
    3. SAGA 模式

      • 推荐程度:适用于长事务、补偿机制复杂、不需要强一致性(最终一致性即可)的业务流程。
      • 原理:将一个分布式事务分解为多个本地事务,每个本地事务都有一个对应的补偿操作。当某个本地事务失败时,通过执行之前已成功事务的补偿操作来回滚整个业务流程。
      • 优点:无需锁定资源,性能高,支持异步处理。
      • 缺点:最终一致性,补偿逻辑复杂,需要业务方设计和实现补偿方案。
    4. XA 模式

      • 推荐程度:Seata 也支持 XA 模式,但其本质与传统的 XA 事务管理器相同,通常不作为微服务下的主要推荐。
AT 模式示例代码(基于 Spring Cloud + Seata)

AT 模式的核心是通过 Seata 代理数据源自动生成 undo/redo 日志,无需手动编写补偿逻辑,对业务代码侵入性极低。

  1. 业务场景 模拟电商下单流程:创建订单(订单服务)→ 扣减库存(库存服务),两个服务跨库操作,通过 AT 模式保证一致性。

  2. 订单服务代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private InventoryFeignClient inventoryFeignClient; // 调用库存服务的Feign客户端

    // 标记@GlobalTransactional开启分布式事务
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(Long userId, Long productId, Integer count) {
        // 1. 创建订单(本地事务)
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setStatus(0); // 0-未支付,1-已支付
        orderMapper.insert(order);
        System.out.println("订单创建成功:" + order.getId());

        // 2. 远程调用库存服务扣减库存
        boolean deductSuccess = inventoryFeignClient.deduct(productId, count);
        if (!deductSuccess) {
            throw new RuntimeException("库存不足,创建订单失败");
        }

        // 3. 若此处抛异常,Seata会自动回滚订单和库存操作
        // int i = 1 / 0; // 测试回滚
    }
}
  1. 库存服务代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class InventoryService {
    @Autowired
    private InventoryMapper inventoryMapper;

    // 本地事务方法,被远程调用
    @Transactional(rollbackFor = Exception.class)
    public boolean deduct(Long productId, Integer count) {
        // 检查库存
        Inventory inventory = inventoryMapper.selectById(productId);
        if (inventory.getStock() < count) {
            return false;
        }
        // 扣减库存
        inventory.setStock(inventory.getStock() - count);
        inventoryMapper.updateById(inventory);
        System.out.println("库存扣减成功:" + productId);
        return true;
    }
}
  1. 核心配置(application.yml)
1
2
3
4
5
6
7
8
9
seata:
  tx-service-group: my_test_tx_group # 事务组需与Seata服务器配置一致
  service:
    vgroup-mapping:
      my_test_tx_group: default # 映射到默认集群
  registry:
    type: nacos # 注册中心与微服务一致
    nacos:
      server-addr: 127.0.0.1:8848

AT 模式关键点:

  • 仅需在发起分布式事务的方法上添加 @GlobalTransactional 注解,无其他业务侵入。
  • Seata 会自动拦截 SQL,生成 undo 日志(用于回滚),并通过 TC(事务协调者)协调两阶段提交。
TCC 模式示例代码(基于 Spring Cloud + Seata)

TCC 模式需要手动实现 Try(资源检查 + 预留)、Confirm(确认提交)、Cancel(补偿回滚)三个阶段,侵入业务代码但性能更高。

  1. 业务场景 同样是电商下单,但要求更灵活的资源控制(如预留库存避免超卖),使用 TCC 模式。

  2. 订单服务 TCC 接口定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// TCC接口(需用@LocalTCC标记)
@LocalTCC
public interface OrderTccService {
    // Try阶段:创建订单并预留库存(实际扣减由库存服务的Try完成)
    @TwoPhaseBusinessAction(name = "createOrderTcc", commitMethod = "confirm", rollbackMethod = "cancel")
    void tryCreateOrder(@BusinessActionContextParameter(paramName = "order") Order order);

    // Confirm阶段:确认订单创建(如更新订单状态为“已确认”)
    void confirm(BusinessActionContext context);

    // Cancel阶段:回滚订单(删除未确认的订单)
    void cancel(BusinessActionContext context);
}
  1. 订单服务 TCC 实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Service
public class OrderTccServiceImpl implements OrderTccService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private InventoryTccFeignClient inventoryTccFeignClient; // 库存服务TCC接口

    @Override
    public void tryCreateOrder(Order order) {
        // 1. Try阶段:创建“待确认”状态的订单(资源预留)
        order.setStatus(2); // 2-待确认(区别于AT模式的0-未支付)
        orderMapper.insert(order);
        System.out.println("TCC Try:订单预创建成功,ID=" + order.getId());

        // 2. 调用库存服务的Try阶段,预留库存
        boolean inventoryTrySuccess = inventoryTccFeignClient.tryDeduct(order.getProductId(), order.getCount());
        if (!inventoryTrySuccess) {
            throw new RuntimeException("TCC Try:库存不足,订单创建失败");
        }
    }

    @Override
    public void confirm(BusinessActionContext context) {
        // 从上下文获取Try阶段的订单信息
        Order order = JSON.parseObject(context.getActionContext("order").toString(), Order.class);
        // 确认订单:更新状态为“已创建”
        order.setStatus(1);
        orderMapper.updateById(order);
        System.out.println("TCC Confirm:订单确认成功,ID=" + order.getId());
    }

    @Override
    public void cancel(BusinessActionContext context) {
        // 从上下文获取Try阶段的订单信息
        Order order = JSON.parseObject(context.getActionContext("order").toString(), Order.class);
        // 回滚订单:删除预创建的订单
        orderMapper.deleteById(order.getId());
        System.out.println("TCC Cancel:订单回滚成功,ID=" + order.getId());
    }
}
  1. 库存服务 TCC 实现(核心部分)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@LocalTCC
public interface InventoryTccService {
    // Try阶段:检查库存并预留(如将可用库存减少,冻结库存增加)
    @TwoPhaseBusinessAction(name = "deductInventoryTcc", commitMethod = "confirmDeduct", rollbackMethod = "cancelDeduct")
    boolean tryDeduct(@BusinessActionContextParameter(paramName = "productId") Long productId,
                      @BusinessActionContextParameter(paramName = "count") Integer count);

    // Confirm阶段:确认扣减(将冻结库存清零,完成实际扣减)
    void confirmDeduct(BusinessActionContext context);

    // Cancel阶段:取消扣减(释放预留的库存)
    void cancelDeduct(BusinessActionContext context);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Service
public class InventoryTccServiceImpl implements InventoryTccService {
    @Autowired
    private InventoryMapper inventoryMapper;

    @Override
    public boolean tryDeduct(Long productId, Integer count) {
        // Try阶段:检查可用库存,并冻结对应数量(资源预留)
        Inventory inventory = inventoryMapper.selectById(productId);
        if (inventory.getAvailableStock() < count) {
            return false;
        }
        // 可用库存 = 可用库存 - count(实际扣减延迟到Confirm)
        // 冻结库存 = 冻结库存 + count(标记为“已预留”)
        inventory.setAvailableStock(inventory.getAvailableStock() - count);
        inventory.setFrozenStock(inventory.getFrozenStock() + count);
        inventoryMapper.updateById(inventory);
        System.out.println("TCC Try:库存预留成功,产品ID=" + productId);
        return true;
    }

    @Override
    public void confirmDeduct(BusinessActionContext context) {
        // Confirm阶段:确认扣减(将冻结库存清零,完成最终扣减)
        Long productId = Long.valueOf(context.getActionContext("productId").toString());
        Integer count = Integer.valueOf(context.getActionContext("count").toString());
        Inventory inventory = inventoryMapper.selectById(productId);
        inventory.setFrozenStock(inventory.getFrozenStock() - count); // 释放冻结库存
        inventoryMapper.updateById(inventory);
        System.out.println("TCC Confirm:库存扣减确认成功,产品ID=" + productId);
    }

    @Override
    public void cancelDeduct(BusinessActionContext context) {
        // Cancel阶段:回滚(释放预留的可用库存,冻结库存清零)
        Long productId = Long.valueOf(context.getActionContext("productId").toString());
        Integer count = Integer.valueOf(context.getActionContext("count").toString());
        Inventory inventory = inventoryMapper.selectById(productId);
        inventory.setAvailableStock(inventory.getAvailableStock() + count); // 恢复可用库存
        inventory.setFrozenStock(inventory.getFrozenStock() - count); // 清空冻结库存
        inventoryMapper.updateById(inventory);
        System.out.println("TCC Cancel:库存扣减回滚成功,产品ID=" + productId);
    }
}

总结

Spring 框架是 Java 生态的基石,必知必会。