WD
Classnote Docs课程课件
13

Spring 整合 MyBatis 与事务管理

学习目标:

  • 掌握 Spring 整合 MyBatis 的配置方法
  • 理解声明式事务的使用和原理
  • 掌握事务传播行为的使用场景
  • 能解释为什么事务本质上也是基于 AOP 代理实现的

本章重点:

  • DataSource、SqlSessionFactoryBean、MapperScan 的整合链路
  • @Transactional 的执行原理与常见坑点
  • REQUIRED / REQUIRES_NEW / NESTED 等传播行为的区别
  • 声明式事务配置、传播行为与常见事务边界问题
01 / Section

1. Spring整合MyBatis

1.1 引入依赖

xml
<dependencies>
    <!-- Spring 6.x 对应使用 MyBatis-Spring 3.0.x -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>3.0.5</version>
    </dependency>
    
    <!-- Spring JDBC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.2.15</version>
    </dependency>
    
    <!-- 数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.11</version>
    </dependency>
    
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.14</version>
    </dependency>
</dependencies>

1.2 配置类

java
@Configuration
@ComponentScan("com.example")
@MapperScan("com.example.mapper")  // 扫描Mapper接口
public class AppConfiguration {
    
    /**
     * 注册数据源
     */
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    /**
     * 注册SqlSessionFactory
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean;
    }
}

如果你不想直接在配置类上使用 @MapperScan,也可以把扫描器本身注册为容器中的组件。这样做的本质,是把“扫描哪个包下的 Mapper 接口”这件事交给一个专门的 MapperScannerConfigurer 组件来完成。

java
@Configuration
@ComponentScan("com.example")
public class AppConfiguration {

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.example.mapper");
        return configurer;
    }

    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean;
    }
}

两种写法的目标是一致的:都是把 Mapper 接口注册为 Spring 容器中的代理对象。区别只是一个更偏注解声明,一个更偏显式配置。

1.3 Mapper接口与使用

java
// Mapper接口
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #{id}")
    User selectById(Integer id);
    
    @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
    int insert(User user);
}

// Service直接使用
@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserMapper userMapper;  // 直接注入使用
    
    @Override
    public User getUserById(Integer id) {
        return userMapper.selectById(id);
    }
}

1.4 整合原理

Spring 整合 MyBatis 的组件调用链路图

最底层先准备 DataSource,它负责提供数据库连接;在它之上,SqlSessionFactoryBean 负责把 MyBatis 的会话工厂交给 Spring 管理;再往上,Mapper 接口会被扫描并注册成可注入的代理对象;最后 Service 层直接注入这些 Mapper 代理对象完成数据库访问。也就是说,业务层并不会直接操作 JDBC 或 SqlSession,而是通过 Spring 容器把整条持久层调用链串接起来。

本章小结

本章核心概念:Spring 整合 MyBatis 的关键,不是零散记住几个注解,而是理解数据源、会话工厂、Mapper 代理对象和 Service 调用之间是如何被 Spring 串起来的。

你现在应该掌握

  • 能说清 DataSourceSqlSessionFactoryBean@MapperScan 各自负责什么
  • 能理解为什么 Mapper 接口可以直接被 @Autowired 注入使用
  • 能画出从 Spring 容器到 Mapper 代理对象再到 Service 的基本整合链路
02 / Section

2. Spring核心接口

  • PlatformTransactionManager 平台事务管理器
  • TransactionStatus 事务状态
  • TransactionDefinition 事务定义

2.1 事务基础回顾

阶段 JDBC代码 MySQL命令
开启事务 conn.setAutoCommit(false) START TRANSACTION
提交事务 conn.commit() COMMIT
回滚事务 conn.rollback() ROLLBACK

后续在连接池的内容中,是DataSource来管理connection,也就是说谁管理了DataSource谁就管理了事务。

那么谁管理了DataSource呢,我们接下来继续来看事务的核心接口

2.2 PlatformTransactionManager 平台事务管理器

平台事务管理器,Spring要管理事务,必须使用事务管理器 有多种实现,通过实现此接口,Spring可以管理任何实现了这些接口的事务。 开发人员可以使用统一的编程模型来控制管理事务。

常见的事务管理器的实现 DataSourceTransactionManager,jdbc开发时事务管理器,使用JdbcTemplate、MyBatis(SSM) HibernateTransactionManager,Hibernate开发时事务管理器,整合Hibernate(SSH)

java
public interface PlatformTransactionManager extends TransactionManager {
  // 开启事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
  // 提交事务
    void commit(TransactionStatus var1) throws TransactionException;
  // 回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}

2.3 TransactionStatus 事务状态

获取事务的状态(回滚点、是否完成、是否新事务、是否回滚)属性,是一个过程值

java
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
  boolean hasSavepoint();

  void flush();
}

提供了关于事务状态的方法

image-20240829143413595
image-20240829143413595

2.4 TransactionDefinition 事务定义

定义事务的名称、隔离级别、传播行为、超时时间长短、只读属性等

java
public interface TransactionDefinition {
  int PROPAGATION_REQUIRED = 0;
  int PROPAGATION_SUPPORTS = 1;
  int PROPAGATION_MANDATORY = 2;
  int PROPAGATION_REQUIRES_NEW = 3;
  int PROPAGATION_NOT_SUPPORTED = 4;
  int PROPAGATION_NEVER = 5;
  int PROPAGATION_NESTED = 6;
  int ISOLATION_DEFAULT = -1;
  int ISOLATION_READ_UNCOMMITTED = 1;
  int ISOLATION_READ_COMMITTED = 2;
  int ISOLATION_REPEATABLE_READ = 4;
  int ISOLATION_SERIALIZABLE = 8;
  int TIMEOUT_DEFAULT = -1;

  default int getPropagationBehavior() {
    return 0;
  }

  default int getIsolationLevel() {
    return -1;
  }

  default int getTimeout() {
    return -1;
  }

  default boolean isReadOnly() {
    return false;
  }

  @Nullable
  default String getName() {
    return null;
  }

  static TransactionDefinition withDefaults() {
    return StaticTransactionDefinition.INSTANCE;
  }
}

手动管理事务

DataSource管理了事务,而PlatformTransactionManager依赖于DataSource组件,其实是PlatformTransactionManager对应的实例来管理事务。

我们向容器中注册一个DataSourceTransactionManager(它是PlatformTransactionManager的实现类),注册这个组件的时候需要依赖于DataSource组件。

java
// 平台事务管理器,Spring要管理事务,必须要使用到这个组件
// dataSource就是连接池
// 管理事务,其实就是管理连接
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
  DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
  transactionManager.setDataSource(dataSource);
  return transactionManager;
}

手动管理事务也就意味着使用PlatformTransactionManager提供的方法来管理事务

java
@Service
public class MarketAccountServiceImpl implements MarketAccountService {

  @Autowired
  AccountMapper accountMapper;

  // 从容器中获取平台事务管理器组件(DataSourceTransactionManager)
  @Autowired
  PlatformTransactionManager transactionManager;

  @Override
  public int transfer(Integer fromId, Integer destId, Integer money) {
    // 先查询出来信息
    MarketAccount fromAccount = accountMapper.selectByPrimaryKey(fromId);
    MarketAccount destAccount = accountMapper.selectByPrimaryKey(destId);

    // fromAccount它的钱 少了
    fromAccount.setMoney(fromAccount.getMoney() - money);
    fromAccount.setUpdateTime(LocalDateTime.now());
    // destAccount它的钱 多了
    destAccount.setMoney(destAccount.getMoney() + money);
    destAccount.setUpdateTime(LocalDateTime.now());

    TransactionStatus transactionStatus = null;
    try {
      // 开启事务
      transactionStatus = transactionManager.getTransaction(TransactionDefinition.withDefaults());
      // 执行更新操作
      accountMapper.updateByPrimaryKeySelective(fromAccount);
      // 制造异常
      int i = 1 / 0;
      accountMapper.updateByPrimaryKeySelective(destAccount);
      // 提交事务
      transactionManager.commit(transactionStatus);
    } catch (Exception e) {
      // 回滚事务
      transactionManager.rollback(transactionStatus);
      throw new RuntimeException(e);
    }
    return 0;
  }

}

那么如果说其他的方法也做类似的事情呢

也在执行业务代码之前开启事务,然后对业务代码增加try-catch,如果执行成功提交事务,失败则回滚事务

java
TransactionStatus transactionStatus = null;
try {
  transactionStatus = transactionManager.getTransaction(TransactionDefinition.withDefaults());
  // 核心业务
  transactionManager.commit(transactionStatus);
} catch (Exception e) {
  transactionManager.rollback(transactionStatus);
  throw new RuntimeException(e);
}

比如下面的一个修改业务

java
@Override
public int modify(Integer id, Integer money) {
  TransactionStatus transactionStatus = null;
  try {
    transactionStatus = transactionManager.getTransaction(TransactionDefinition.withDefaults());
    MarketAccount marketAccount = new MarketAccount();
    marketAccount.setId(id);
    marketAccount.setMoney(money);
    accountMapper.updateByPrimaryKeySelective(marketAccount);
    transactionManager.commit(transactionStatus);
  } catch (Exception e) {
    transactionManager.rollback(transactionStatus);
    throw new RuntimeException(e);
  }
  return 0;
}

这里的方法都使用相同的业务,这时候我们要想要AOP技术

要增加事务的方法 放入到切入点

事务的业务 就是通知

Spring其实也提供了对应的方案,那就是Spring事务\

03 / Section

3. Spring声明式事务

3.1 启用声明式事务

java
@Configuration
@ComponentScan("com.example")
@MapperScan("com.example.mapper")
@EnableTransactionManagement  // 开启声明式事务
public class AppConfiguration {
    
    // ... DataSource配置
    
    // ... SqlSessionFactory配置
    
    /**
     * 注册事务管理器(必须)
     */
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager manager = new DataSourceTransactionManager();
        manager.setDataSource(dataSource);
        return manager;
    }
}

3.2 @Transactional 使用

java
@Service
public class TransferServiceImpl implements TransferService {
    
    @Autowired
    private AccountMapper accountMapper;
    
    /**
     * @Transactional让方法在事务中执行
     * - 方法执行前:开启事务
     * - 方法正常结束:提交事务
     * - 方法抛出异常:回滚事务
     */
    @Transactional
    @Override
    public void transfer(String from, String to, Double amount) {
        // 扣款
        accountMapper.decreaseBalance(from, amount);
        
        // 模拟异常
        // int i = 1 / 0;  // 这行会导致事务回滚
        
        // 收款
        accountMapper.increaseBalance(to, amount);
    }
}

3.3 声明式事务原理

diagram
@Transactional方法 调用时实际执行的是代理对象的方法: 1. 开启事务 2. 执行业务方法 3. 判断结果: 正常结束 提交事务 抛出异常 回滚事务
┌──────────────────────────────────────────┐
│           @Transactional方法              │
│                                          │
│   调用时实际执行的是代理对象的方法:          │
│                                          │
│   1. 开启事务                             │
│       ↓                                  │
│   2. 执行业务方法                          │
│       ↓                                  │
│   3. 判断结果:                            │
│      ├─ 正常结束 → 提交事务                │
│      └─ 抛出异常 → 回滚事务                │
│                                          │
└──────────────────────────────────────────┘

这张图的关键不是记住三个步骤,而是先理解:当方法被 @Transactional 标记后,外部调用时真正进入的并不是原始业务对象,而是 Spring 生成的代理对象。代理对象会先决定是否开启事务,再去调用真实业务方法;方法返回后,根据执行结果决定提交还是回滚。也就是说,事务控制逻辑始终包裹在业务方法外围,而不是写死在业务代码内部。

Spring通过AOP生成代理对象,对@Transactional方法做环绕增强:

  • 方法执行前:调用PlatformTransactionManager.getTransaction()开启事务
  • 方法执行后:调用commitTransactionAfterReturning()提交事务
  • 抛出异常时:调用completeTransactionAfterThrowing()回滚事务

3.4 @Transactional 属性

属性 说明 默认值
propagation 事务传播行为 Propagation.REQUIRED
isolation 事务隔离级别 Isolation.DEFAULT
readOnly 是否只读事务 false
timeout 超时时间(秒) -1(无限制)
rollbackFor 哪些异常触发回滚 RuntimeExceptionError
java
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    readOnly = false,
    timeout = 30,
    rollbackFor = Exception.class
)
public void transfer(...) { }

本章小结

本章核心概念:声明式事务的价值在于把“开启事务、提交事务、回滚事务”这套固定流程从业务代码中抽离出来,让开发者只关注业务边界。

你现在应该掌握

  • 能说明 @EnableTransactionManagementPlatformTransactionManager 为什么缺一不可
  • 能理解 @Transactional 方法为什么最终还是依赖 AOP 代理生效
  • 能根据常见属性判断事务隔离级别、传播行为和回滚规则的作用
04 / Section

4. 事务传播行为

4.1 什么是传播行为

事务传播行为定义了:当一个事务方法调用另一个事务方法时,事务如何传播

java
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional  // 开启事务T1
    public void methodA() {
        // ... 操作数据库
        serviceB.methodB();  // 调用另一个事务方法
    }
}

@Service
public class ServiceB {
    @Transactional  // 怎么处理?加入T1?新建T2?
    public void methodB() {
        // ... 操作数据库
    }
}

4.2 7种传播行为

传播行为 含义 使用场景
REQUIRED(默认) 当前有事务则加入,无则新建 大多数场景
REQUIRES_NEW 始终新建事务,挂起当前事务 需要独立提交/回滚
NESTED 当前有事务则创建嵌套事务(保存点) 子事务可单独回滚
SUPPORTS 有事务则加入,无则以非事务执行 不强制要求事务
NOT_SUPPORTED 以非事务执行,挂起当前事务 不需要事务的方法
MANDATORY 强制要求存在事务,无则抛异常 必须在事务中执行
NEVER 强制要求无事务,有则抛异常 不允许在事务中执行

4.3 REQUIRED(默认)

java
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional  // 默认REQUIRED,新建事务T1
    public void methodA() {
        serviceB.methodB();  // 加入T1,不新建事务
    }
}

@Service
public class ServiceB {
    @Transactional  // 默认REQUIRED
    public void methodB() {
        // 复用methodA的事务T1
    }
}

特点

  • methodA和methodB共用同一个事务
  • 任一方法抛出异常,整个事务回滚

4.4 REQUIRES_NEW

java
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional
    public void methodA() {
        // 在事务T1中
        serviceB.methodB();  // 挂起T1,新建T2
        // 恢复T1
    }
}

@Service
public class ServiceB {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 在独立的事务T2中
    }
}

特点

  • methodB在独立事务T2中执行
  • T1和T2互不影响,各自提交或回滚
  • 适用场景:日志记录,无论主业务成功与否都要记录

4.5 NESTED

java
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional
    public void methodA() {
        // 外层事务
        serviceB.methodB();  // 创建嵌套事务(保存点)
        // 继续外层事务
    }
}

@Service
public class ServiceB {
    @Transactional(propagation = Propagation.NESTED)
    public void methodB() {
        // 嵌套事务
    }
}

特点

  • 子事务回滚不影响父事务
  • 父事务回滚,子事务也回滚
  • 基于JDBC保存点(Savepoint)实现

三者对比

diagram
REQUIRED: REQUIRES_NEW: NESTED:methodA methodA methodA (事务T1) (事务T1) (事务T1) methodB 挂起T1 保存点1 (共用T1) methodB methodB (事务T2) (嵌套事务)恢复T1 回滚到 保存点1
REQUIRED:                    REQUIRES_NEW:                NESTED:
┌─────────┐                  ┌─────────┐                  ┌─────────┐
│methodA  │                  │methodA  │                  │methodA  │
│(事务T1) │                  │(事务T1) │                  │(事务T1) │
│    ↓    │                  │    ↓    │                  │    ↓    │
│methodB  │                  │挂起T1   │                  │保存点1  │
│(共用T1) │                  │    ↓    │                  │    ↓    │
└─────────┘                  │methodB  │                  │methodB  │
                             │(事务T2) │                  │(嵌套事务)│
                             │    ↓    │                  │    ↓    │
                             │恢复T1   │                  │回滚到    │
                             └─────────┘                  │保存点1   │
                                                          └─────────┘

本章小结

本章核心概念:传播行为本质上是在回答“一个事务方法调用另一个事务方法时,到底共用原事务、挂起原事务,还是在原事务内部建立保存点”。

你现在应该掌握

  • 能区分 REQUIREDREQUIRES_NEWNESTED 三种最常见传播行为
  • 能根据业务目标判断是共用事务还是拆成独立事务
  • 能理解传播行为选择错误时为什么会带来回滚范围失控的问题
05 / Section

附录:核心注解速查表

注解 作用 使用位置
@MapperScan 扫描MyBatis Mapper接口 配置类
@EnableTransactionManagement 开启声明式事务 配置类
@Transactional 标记事务方法 类/方法
@Mapper 标记Mapper接口 接口
06 / Section

实战练习

<!-- 实战练习内容已分离到 ../practices/markdown/13-spring-mybatis-practice.md -->

建议先完成本章重点内容复习,再进入配套练习。 练习建议按照:基础 → 进阶 → 综合挑战 的顺序完成。