Java 踩坑 1|Spring 事务导致多数据源切换失败

背景

在我的日常开发中,遇到了同一个应用需要接入多数据源,因此需要指定不同数据访问层的数据源,而同时又需要在服务层保证事务,因此在服务层方法上使用了 @Transactional 注解,然而在实际执行时却没有切换数据源。

场景复现

这里为了简便,直接使用开源组件 dynamic-datasource-spring-boot-starter 实现多数据源切换,大家也可以自己动手实现。

1)创建 SpringBoot 工程;

2)引入 dynamic-datasource 依赖:

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>

3)按照 dynamic-datasource 规范添加数据源配置;

yml 复制代码
spring:
  datasource:
    dynamic:
      datasource:
        master:
          url: jdbc:h2:mem:master
          driver-class-name: org.h2.Driver
          init:
            schema: classpath:master-schema-h2.sql
            data: classpath:master-data-h2.sql
        slave_1:
          url: jdbc:h2:mem:slave_1
          driver-class-name: org.h2.Driver
          init:
            schema: classpath:slave_1-schema-h2.sql
            data: classpath:slave_1-data-h2.sql
      strict: false
      primary: master

4)添加 DDL 及数据:

sql 复制代码
-- master-schema-h2.sql
DROP TABLE IF EXISTS master_user;

CREATE TABLE master_user
(
    id BIGINT NOT NULL COMMENT '主键ID',
    user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);
-- master-data-h2.sql
DELETE FROM master_user;

INSERT INTO master_user (id, user_name, age, email) VALUES
                                            (1, 'Jone', 18, 'test1@baomidou.com'),
                                            (2, 'Jack', 20, 'test2@baomidou.com'),
                                            (3, 'Tom', 28, 'test3@baomidou.com'),
                                            (4, 'Sandy', 21, 'test4@baomidou.com'),
                                            (5, 'Billie', 24, 'test5@baomidou.com');
-- slave_1-schema-h2.sql
DROP TABLE IF EXISTS slave_user;

CREATE TABLE slave_user
(
    id BIGINT NOT NULL COMMENT '主键ID',
    user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);
-- slave_1-data-h2.sql
DELETE FROM slave_user;

INSERT INTO slave_user (id, user_name, age, email) VALUES
                                            (6, 'Jone', 18, 'test1@baomidou.com'),
                                            (7, 'Jack', 20, 'test2@baomidou.com'),
                                            (8, 'Tom', 28, 'test3@baomidou.com'),
                                            (9, 'Sandy', 21, 'test4@baomidou.com'),
                                            (10, 'Billie', 24, 'test5@baomidou.com');

4)创建数据对象及 Mapper,这里只列出了 Master 数据源数据对象及 Mapper;

java 复制代码
package com.itschenxiang.multidatasource.entity;

import lombok.Data;

@Data
public class MasterUser {
    private Long id;
    private String userName;
    private Integer age;
    private String email;
}
java 复制代码
package com.itschenxiang.multidatasource.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itschenxiang.multidatasource.entity.MasterUser;
import org.springframework.stereotype.Repository;

@Repository
public interface MasterUserMapper extends BaseMapper<MasterUser> {

}

5)新增服务层构建场景;

java 复制代码
@Service
public class MultiDataSourceService {

    @Autowired
    private MasterUserMapper masterUserMapper;

    @Autowired
    private SlaveUserMapper slaveUserMapper;
    
    @Autowired
    @Lazy
    private MultiDataSourceService multiDataSourceService;
    
    // 单master数据源
    @DS("master")
    public List<MasterUser> accessPrimaryDataSource() {
        return masterUserMapper.selectList(null);
    }

    // 单slave_1数据源
    @DS("slave_1")
    public List<SlaveUser> accessNotPrimaryDataSource() {
        return slaveUserMapper.selectList(null);
    }

    // 多数据源,无@Transactional注解
    public void multiDataSourceWithoutTransactional() {
        multiDataSourceService.accessPrimaryDataSource();
        multiDataSourceService.accessNotPrimaryDataSource();
    }

    // 多数据源,有@Transactional注解,mapper执行出错
    @Transactional(rollbackFor = Exception.class)
    public void multiDataSourceWithTransactional() {
        accessPrimaryDataSource();
        accessNotPrimaryDataSource();
    }
}

6)添加单元测试复现问题;这里仅列举了 @Transactional 导致异常的 UT;

java 复制代码
@SpringBootTest
@ActiveProfiles("ut")
@RunWith(SpringRunner.class)
public class MultiDataSourceServiceTest {

    @Autowired
    private MultiDataSourceService multiDataSourceService;
  
    @Test
    public void multiDataSourceWithTransactionalTest() {
        try {
            multiDataSourceService.multiDataSourceWithTransactional();
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue(e instanceof BadSqlGrammarException);
        }
    }
    
}

复现代码链接:github.com/itschenxian...

根因分析

Spring 开启事务后会维护一个 ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。也就是说:使用了 @Transactional,Spring 会保证整个事务下都复用同一个 connection

需要额外注意的是:单库的事务仍然可用,只要事务下不切换数据源即可。

解决方案

对于确实需要单事务多数据源的场景,解决方案包括:

  1. 从文章标题就可以看到的解决方案:删除事务注解;
  2. Seata 事务;

其实对于大部分合理业务场景,应用可能有多个数据源,但基本都是单数据源事务。我实际遇到的也是单数据源事务,数据源切换失败的问题,根本原因是自定义数据源切换切面执行顺序在 @Transactional 之后,导致无法切换数据源(参考链接1)。

参考链接

相关推荐
gjxDaniel14 分钟前
Go编程语言入门与常见问题
开发语言·后端·go
sunnyday04262 小时前
Spring Boot 自定义 Starter 实战:从创建到使用的完整指南
spring boot·后端·mybatis
想用offer打牌3 小时前
2025年总结:一个树苗倔强生长
java·后端·开源·go
小北方城市网3 小时前
Redis 分布式锁与缓存三大问题解决方案
spring boot·redis·分布式·后端·缓存·wpf·mybatis
哪里不会点哪里.3 小时前
Spring 核心原理解析:它到底解决了什么问题?
java·后端·spring
小杍随笔3 小时前
【Rust Cargo 目录迁移到 D 盘:不改变安装路径和环境变量的终极方案】
开发语言·后端·rust
沐雨风栉4 小时前
用 Kavita+cpolar 把数字书房装进口袋
服务器·开发语言·数据库·后端·golang
郑州光合科技余经理5 小时前
同城020系统架构实战:中台化设计与部署
java·大数据·开发语言·后端·系统架构·uni-app·php
Mr -老鬼6 小时前
UpdateEC - EasyClick 项目热更新系统(Rust构建)
开发语言·后端·rust
一 乐6 小时前
学生宿舍管理|基于springboot + vue学生宿舍管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·助农电商系统