Skip to content
Go back

Spring多数据源事务配置与回滚测试实战

Published:  at  11:46 PM

Spring多数据源事务配置与回滚测试实战

在Spring项目中配置多数据源并保证事务的正确性,是开发中常见的需求,也是容易踩坑的环节。本文将从多数据源配置、事务管理器绑定、事务回滚测试三个维度,完整讲解Spring多数据源事务的实现与验证,同时揭示常见的事务失效问题及解决方案。

一、场景背景

在实际开发中,我们可能需要同时操作多个数据库(如本文中的java库和sql50库),每个数据库需要独立的数据源和事务管理器,且要保证事务的原子性(异常时能正确回滚)。本文将基于Spring Boot + JdbcTemplate实现多数据源配置,并通过测试验证事务回滚效果。

二、核心配置实现

2.1 多数据源与事务管理器配置

首先创建数据源配置类,分别配置两个数据源、对应的JdbcTemplate和事务管理器,注意每个事务管理器必须绑定对应的数据源。

package org.example.multidb.db;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@Configuration
public class DatasourceConfig {

    // 主数据源(db1:java库),@Primary标注默认数据源
    @Bean("db1")
    @Primary
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/java");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    // db1对应的JdbcTemplate
    @Bean("db1JdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("db1") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    // db1对应的事务管理器
    @Bean("transactionManager")
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    // 第二个数据源(db2:sql50库)
    @Bean("db2")
    public DataSource dataSource2() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/sql50");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    // db2对应的JdbcTemplate
    @Bean("db2JdbcTemplate")
    public JdbcTemplate jdbcTemplate2(@Qualifier("db2") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    // db2对应的事务管理器(关键:绑定dataSource2()而非dataSource())
    @Bean("transactionManager2")
    public DataSourceTransactionManager transactionManager2() {
        return new DataSourceTransactionManager(dataSource2());
    }
}

关键注意点

2.2 事务业务层实现

创建事务服务类,封装事务方法和通用操作方法。核心:事务方法必须放在Spring管理的Bean中,且通过注入调用(避免内部调用导致事务失效)

package org.example.multidb.service;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionTestService {

    // 通用方法:查询表数据量
    public int countData(JdbcTemplate jdbcTemplate) {
        return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM test_transaction", Integer.class);
    }

    // 初始化方法:清空测试表
    public void initTable(JdbcTemplate jdbcTemplate) {
        jdbcTemplate.execute("TRUNCATE TABLE test_transaction");
    }

    // db1事务方法:指定transactionManager,异常时回滚
    @Transactional(transactionManager = "transactionManager", rollbackFor = RuntimeException.class)
    public void executeDb1Transaction(JdbcTemplate jdbcTemplate) {
        // 插入测试数据
        String insertSql = "INSERT INTO test_transaction (content) VALUES ('db1_test_data')";
        jdbcTemplate.execute(insertSql);

        // 打印回滚前数据量
        int beforeRollbackCount = countData(jdbcTemplate);
        System.out.println("回滚前(事务内):test_transaction 表数据量:" + beforeRollbackCount);

        // 主动抛异常触发回滚
        throw new RuntimeException("主动触发 db1 事务回滚");
    }

    // db2事务方法:指定transactionManager2,异常时回滚
    @Transactional(transactionManager = "transactionManager2", rollbackFor = RuntimeException.class)
    public void executeDb2Transaction(JdbcTemplate jdbcTemplate) {
        // 插入测试数据
        String insertSql = "INSERT INTO test_transaction (content) VALUES ('db2_test_data')";
        jdbcTemplate.execute(insertSql);

        // 打印回滚前数据量
        int beforeRollbackCount = countData(jdbcTemplate);
        System.out.println("回滚前(事务内):test_transaction 表数据量:" + beforeRollbackCount);

        // 主动抛异常触发回滚
        throw new RuntimeException("主动触发 db2 事务回滚");
    }

    // 错误示例:db2操作但使用db1的事务管理器
    @Transactional(transactionManager = "transactionManager", rollbackFor = RuntimeException.class)
    public void executeDb2Transaction2(JdbcTemplate jdbcTemplate) {
        // 插入测试数据
        String insertSql = "INSERT INTO test_transaction (content) VALUES ('db2_test_data')";
        jdbcTemplate.execute(insertSql);

        // 打印回滚前数据量
        int beforeRollbackCount = countData(jdbcTemplate);
        System.out.println("回滚前(事务内):test_transaction 表数据量:" + beforeRollbackCount);

        // 主动抛异常触发回滚
        throw new RuntimeException("主动触发 db2 事务回滚");
    }
}

关键配置说明

三、事务回滚测试

3.1 测试准备

java库和sql50库中分别创建测试表:

CREATE TABLE IF NOT EXISTS test_transaction (
    id INT PRIMARY KEY AUTO_INCREMENT,
    content VARCHAR(50) NOT NULL,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.2 测试类实现

编写测试类,验证不同场景下的事务回滚效果,重点测试「正确绑定事务管理器」和「错误绑定事务管理器」两种场景。

package org.example.multidb;

import org.example.multidb.service.TransactionTestService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

// 禁用测试方法默认事务(避免干扰业务事务)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@SpringBootTest
public class MultiDbTransactionRollbackTests {

    @Autowired
    private TransactionTestService transactionTestService;

    // 测试db1事务回滚(正确绑定transactionManager)
    @Test
    void testDb1TransactionRollback(@Qualifier("db1JdbcTemplate") JdbcTemplate db1JdbcTemplate) {
        System.out.println("===== 开始测试 db1 (java 库) 事务回滚 =====");

        // 初始化:清空测试表
        transactionTestService.initTable(db1JdbcTemplate);
        System.out.println("初始化:清空 test_transaction 表,初始数据量:" + transactionTestService.countData(db1JdbcTemplate));

        try {
            // 调用事务方法(通过注入的Service调用,触发AOP代理)
            transactionTestService.executeDb1Transaction(db1JdbcTemplate);
        } catch (RuntimeException e) {
            System.out.println("捕获到异常,事务触发回滚:" + e.getMessage());
        }

        // 验证回滚结果
        int afterRollbackCount = transactionTestService.countData(db1JdbcTemplate);
        System.out.println("回滚后:test_transaction 表数据量:" + afterRollbackCount);
        System.out.println("===== db1 事务回滚测试结束 =====\n");
    }

    // 测试db2事务回滚(正确绑定transactionManager2)
    @Test
    void testDb2TransactionRollback(@Qualifier("db2JdbcTemplate") JdbcTemplate db2JdbcTemplate) {
        System.out.println("===== 开始测试 db2 (sql50 库) 事务回滚 =====");

        // 初始化:清空测试表
        transactionTestService.initTable(db2JdbcTemplate);
        System.out.println("初始化:清空 test_transaction 表,初始数据量:" + transactionTestService.countData(db2JdbcTemplate));

        try {
            // 调用事务方法
            transactionTestService.executeDb2Transaction(db2JdbcTemplate);
        } catch (RuntimeException e) {
            System.out.println("捕获到异常,事务触发回滚:" + e.getMessage());
        }

        // 验证回滚结果
        int afterRollbackCount = transactionTestService.countData(db2JdbcTemplate);
        System.out.println("回滚后:test_transaction 表数据量:" + afterRollbackCount);
        System.out.println("===== db2 事务回滚测试结束 =====\n");
    }

    // 测试db2事务回滚(错误绑定transactionManager)
    @Test
    void testDb2TransactionRollbackUsingWrongTxManager(@Qualifier("db2JdbcTemplate") JdbcTemplate db2JdbcTemplate) {
        System.out.println("===== 开始测试 db2 (sql50 库) 事务回滚(错误TxManager) =====");

        // 初始化:清空测试表
        transactionTestService.initTable(db2JdbcTemplate);
        System.out.println("初始化:清空 test_transaction 表,初始数据量:" + transactionTestService.countData(db2JdbcTemplate));

        try {
            // 调用错误的事务方法
            transactionTestService.executeDb2Transaction2(db2JdbcTemplate);
        } catch (RuntimeException e) {
            System.out.println("捕获到异常,事务触发回滚:" + e.getMessage());
        }

        // 验证回滚结果
        int afterRollbackCount = transactionTestService.countData(db2JdbcTemplate);
        System.out.println("回滚后:test_transaction 表数据量:" + afterRollbackCount);
        System.out.println("===== db2 事务回滚测试结束 =====\n");
    }
}

测试类关键配置

四、测试结果与分析

4.1 正确绑定事务管理器(db1/db2)

===== 开始测试 db1 (java 库) 事务回滚 =====
初始化:清空 test_transaction 表,初始数据量:0
回滚前(事务内):test_transaction 表数据量:1
捕获到异常,事务触发回滚:主动触发 db1 事务回滚
回滚后:test_transaction 表数据量:0
===== db1 事务回滚测试结束 =====

===== 开始测试 db2 (sql50 库) 事务回滚 =====
初始化:清空 test_transaction 表,初始数据量:0
回滚前(事务内):test_transaction 表数据量:1
捕获到异常,事务触发回滚:主动触发 db2 事务回滚
回滚后:test_transaction 表数据量:0
===== db2 事务回滚测试结束 =====

结论:正确绑定事务管理器时,异常触发后数据回滚,数据量从1变回0,事务生效。

4.2 错误绑定事务管理器(db2用db1的TxManager)

===== 开始测试 db2 (sql50 库) 事务回滚(错误TxManager) =====
初始化:清空 test_transaction 表,初始数据量:0
回滚前(事务内):test_transaction 表数据量:1
捕获到异常,事务触发回滚:主动触发 db2 事务回滚
回滚后:test_transaction 表数据量:1
===== db2 事务回滚测试结束 =====

结论:事务管理器与数据源不匹配时,异常无法触发回滚,数据被持久化,事务失效。

五、核心踩坑点总结

  1. 事务管理器与数据源绑定错误:每个事务管理器必须绑定对应数据源,否则事务无法管理目标数据库;
  2. 内部调用导致事务失效:事务方法必须放在独立的Spring Bean中,通过注入调用(避免本类内部调用,绕开AOP代理);
  3. 测试方法默认事务干扰:测试类需禁用默认事务(Propagation.NOT_SUPPORTED),否则无法正确验证业务事务的回滚效果;
  4. 未显式指定事务管理器:多数据源场景下,必须通过@Transactional(transactionManager = "xxx")指定事务管理器,否则默认使用主数据源的事务管理器。

六、总结

Spring多数据源事务的核心是「数据源-事务管理器-JdbcTemplate」三者一一对应,且保证事务方法通过Spring代理调用。通过本文的配置和测试,可清晰验证多数据源事务的正确性,同时规避常见的事务失效问题。在实际开发中,需重点关注事务管理器的绑定和AOP代理的触发条件,确保多数据源场景下事务的原子性。


Suggest Changes

Next Post
当 GRUB 找不到 Windows