行业资讯 2025年08月6日
0 收藏 0 点赞 301 浏览 3919 个字
摘要 :

文章目录 背景 场景复现 根因分析 解决方案 背景 在我的日常开发中,遇到了一个问题:我的应用需要接入多个数据源,但在服务层需要保证事务的一致性。我在服务层的方法……




  • 背景
  • 场景复现
  • 根因分析
  • 解决方案

背景

在我的日常开发中,遇到了一个问题:我的应用需要接入多个数据源,但在服务层需要保证事务的一致性。我在服务层的方法上使用了 @Transactional 注解,但实际执行时并没有切换数据源,导致问题出现。从而在此复盘下该问题!

场景复现

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

1)创建 SpringBoot 工程;

2)引入 dynamic-datasource 依赖:

dependency>
  groupId>com.baomidougroupId>
  artifactId>dynamic-datasource-spring-boot-starterartifactId>
  version>${version}version>
dependency>

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

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 及数据:

-- 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;

package com.itschenxiang.multidatasource.entity;

import lombok.Data;

@Data
public class MasterUser {
    private Long id;
    private String userName;
    private Integer age;
    private String email;
}
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 {

}

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

@Service
public class MultiDataSourceService {

    @Autowired
    private MasterUserMapper masterUserMapper;

    @Autowired
    private SlaveUserMapper slaveUserMapper;
    
    @Autowired
    @Lazy
    private MultiDataSourceService multiDataSourceService;
    
    // 单master数据源
    @DS(\"master\")
    public ListaccessPrimaryDataSource() {
        return masterUserMapper.selectList(null);
    }

    // 单slave_1数据源
    @DS(\"slave_1\")
    public ListaccessNotPrimaryDataSource() {
        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;

@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);
        }
    }
    
}

根因分析

经过分析,我发现在 Spring 开启事务后会维护一个 ConnectionHolder,保证整个事务都使用同一个数据库连接。这意味着使用了 @Transactional 注解后,Spring 会保证整个事务都使用同一个 connection。

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

解决方案

针对确实需要单事务多数据源的场景,有以下解决方案:

  1. 删除事务注解:如果在业务场景中不需要事务的一致性,可以考虑直接删除 @Transactional 注解。
  2. 使用 Seata 事务:Seata 是一种分布式事务解决方案,可以解决多数据源下的事务一致性问题。

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

微信扫一扫

支付宝扫一扫

版权: 转载请注明出处:https://www.zuozi.net/8947.html

管理员

相关推荐
2025-08-06

文章目录 一、Reader 接口概述 1.1 什么是 Reader 接口? 1.2 Reader 与 InputStream 的区别 1.3 …

988
2025-08-06

文章目录 一、事件溯源 (一)核心概念 (二)Kafka与Golang的优势 (三)完整代码实现 二、命令…

465
2025-08-06

文章目录 一、证明GC期间执行native函数的线程仍在运行 二、native线程操作Java对象的影响及处理方…

348
2025-08-06

文章目录 一、事务基础概念 二、MyBatis事务管理机制 (一)JDBC原生事务管理(JdbcTransaction)…

456
2025-08-06

文章目录 一、SnowFlake算法核心原理 二、SnowFlake算法工作流程详解 三、SnowFlake算法的Java代码…

517
2025-08-06

文章目录 一、本地Jar包的加载操作 二、本地Class的加载方法 三、远程Jar包的加载方式 你知道Groo…

832
发表评论
暂无评论

还没有评论呢,快来抢沙发~

助力内容变现

将您的收入提升到一个新的水平

点击联系客服

在线时间:08:00-23:00

客服QQ

122325244

客服电话

400-888-8888

客服邮箱

122325244@qq.com

扫描二维码

关注微信客服号