SpringBoot配置Mysql+达梦双数据源

'# SpringBoot配置Mysql+达梦双数据源

一、背景与问题

在分布式系统中,业务系统往往需要同时访问多个数据库。例如某金融系统可能需要同时连接MySQL和达梦数据库(DM),分别用于处理交易日志和核心业务数据。这种场景下,传统的单数据源配置无法满足需求,需要实现双数据源配置。

双数据源配置面临以下核心挑战:

  1. 如何动态切换数据源
  2. 如何处理跨数据源的事务
  3. 如何保证数据一致性
  4. 如何应对不同数据库的差异(如达梦数据库的特殊语法)

二、基本原理

SpringBoot的双数据源方案本质上是基于AbstractRoutingDataSource的多数据源抽象。其核心原理如下:

  1. 定义多个数据源实例(DataSource)
  2. 创建抽象数据源(AbstractRoutingDataSource),通过determineCurrentLookupKey()方法决定使用哪个数据源
  3. 通过AOP或注解实现数据源切换逻辑
  4. 配合事务管理器处理分布式事务

达梦数据库作为国产数据库,其JDBC驱动与MySQL存在差异,需要特别处理连接参数和驱动类。

三、环境准备

1. 依赖配置(pom.xml)

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    
    <!-- 达梦数据库驱动 -->
    <dependency>
        <groupId>com.dameng</groupId>
        <artifactId>dmjdbc</artifactId>
        <version>5.1.1</version>
    </dependency>
    
    <!-- 数据源 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.1.0</version>
    </dependency>
</dependencies>

2. 数据库配置(application.yml)

spring:
  datasource:
    mysql:
      url: jdbc:mysql://localhost:3306/mysql_db?useSSL=false&serverTimezone=UTC
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    dm:
      url: jdbc:dm://localhost:5236/dm_db?characterEncoding=utf8
      username: sysdba
      password: sysdba
      driver-class-name: dm.jdbc.driver.DmDriver

四、核心实现

1. 自定义数据源路由类

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}
public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
    
    public static void setDataSource(String dataSource) {
        CONTEXT.set(dataSource);
    }
    
    public static String getDataSource() {
        return CONTEXT.get();
    }
    
    public static void clearDataSource() {
        CONTEXT.remove();
    }
}

2. 配置双数据源

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.dm")
    public DataSource dmDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(Map.of(
            "mysql", mysqlDataSource(),
            "dm", dmDataSource()
        ));
        dynamicDataSource.setDefaultTargetDataSource(mysqlDataSource());
        return dynamicDataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

3. 使用注解切换数据源

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "mysql";
}
@Aspect
@Component
public class DataSourceAspect {
    @Around("@annotation(dataSource)")
    public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
        String dataSourceName = dataSource.value();
        DataSourceContextHolder.setDataSource(dataSourceName);
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

五、完整案例

1. 项目结构

src/
├── main/
│   ├── java/
│   │   └── com.example.demo/
│   │       ├── config/
│   │       │   └── DataSourceConfig.java
│   │       ├── aspect/
│   │       │   └── DataSourceAspect.java
│   │       ├── annotation/
│   │       │   └── DataSource.java
│   │       ├── service/
│   │       │   └── DualDataSourceService.java
│   │       └── controller/
│   │           └── DualDataSourceController.java
│   └── resources/
│       └── application.yml

2. 完整案例代码

// DualDataSourceService.java
@Service
public class DualDataSourceService {

    @Autowired
    private JdbcTemplate mysqlJdbcTemplate;

    @Autowired
    private JdbcTemplate dmJdbcTemplate;

    @DataSource("mysql")
    public void saveToMysql(String data) {
        mysqlJdbcTemplate.update("INSERT INTO test_table (content) VALUES (?)", data);
    }

    @DataSource("dm")
    public void saveToDm(String data) {
        dmJdbcTemplate.update("INSERT INTO test_table (content) VALUES (?)", data);
    }

    @DataSource("mysql")
    public List<String> queryFromMysql() {
        return mysqlJdbcTemplate.queryForList("SELECT content FROM test_table", String.class);
    }

    @DataSource("dm")
    public List<String> queryFromDm() {
        return dmJdbcTemplate.queryForList("SELECT content FROM test_table", String.class);
    }
}
// DualDataSourceController.java
@RestController
@RequestMapping("/dual")
public class DualDataSourceController {

    @Autowired
    private DualDataSourceService dualDataSourceService;

    @PostMapping("/mysql")
    public void saveToMysql(@RequestParam String data) {
        dualDataSourceService.saveToMysql(data);
    }

    @PostMapping("/dm")
    public void saveToDm(@RequestParam String data) {
        dualDataSourceService.saveToDm(data);
    }

    @GetMapping("/mysql")
    public List<String> queryFromMysql() {
        return dualDataSourceService.queryFromMysql();
    }

    @GetMapping("/dm")
    public List<String> queryFromDm() {
        return dualDataSourceService.queryFromDm();
    }
}

六、源码解析

1. DynamicDataSource类

  • 通过重写determineCurrentLookupKey()方法实现数据源切换
  • 使用ThreadLocal保证线程安全
  • 支持动态切换数据源,适用于多租户、多环境等场景

2. 数据源配置

  • 使用@ConfigurationProperties自动绑定配置
  • 配置两个独立的数据源实例
  • 通过AbstractRoutingDataSource实现多数据源抽象

3. 事务管理

  • 配合DataSourceTransactionManager处理事务
  • 注意:跨数据源事务需要配置分布式事务管理器(如JTA)

七、进阶使用

1. 动态数据源策略

public class DynamicDataSourceStrategy implements RoutingStrategy {
    @Override
    public String determineCurrentLookupKey() {
        // 根据业务逻辑动态选择数据源
        return System.currentTimeMillis() % 2 == 0 ? "mysql" : "dm";
    }
}

2. 数据源监控

@Configuration
public class DataSourceMonitor {
    @Bean
    public DataSourceMonitorBean dataSourceMonitorBean(DataSource dynamicDataSource) {
        return new DataSourceMonitorBean(dynamicDataSource);
    }
}

3. 优化建议

  • 对频繁切换的数据源使用连接池
  • 对不常用的数据源配置空闲超时
  • 对敏感操作增加日志记录

八、性能与工程实践

1. 性能优化

优化项建议原因
连接池配置设置maxPoolSize为100防止资源耗尽
SQL预编译使用PreparedStatement防止SQL注入
查询缓存对频繁查询结果进行缓存减少数据库压力
索引优化对查询字段添加索引提高查询效率

2. 安全风险

  • 数据库连接信息应加密存储
  • 使用Spring Security进行权限控制
  • 对敏感操作增加审计日志
  • 避免使用明文密码,建议使用Vault等密钥管理工具

3. 异常处理

@ExceptionHandler
public ResponseEntity<String> handleException(Exception e) {
    log.error("数据库操作异常", e);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("数据库操作失败");
}

九、常见问题与踩坑

1. 驱动类不匹配

// 错误示例
public class MyDataSource {
    public static void main(String[] args) {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

问题:达梦数据库驱动类名不同
解决:确认驱动类名,达梦驱动为dm.jdbc.driver.DmDriver

2. 连接字符串错误

// 错误示例
String url = "jdbc:dm://localhost:5236/dm_db";

问题:缺少参数配置
解决:添加characterEncoding=utf8参数

String url = "jdbc:dm://localhost:5236/dm_db?characterEncoding=utf8";

3. 事务管理问题

// 错误示例
@Transactional
public void saveData() {
    saveToMysql("test");
    saveToDm("test");
}

问题:跨数据源事务不支持
解决:使用分布式事务管理器(如JTA)

4. 线程安全问题

// 错误示例
public class MyService {
    private String dataSource;
    
    public void setDataSource(String dataSource) {
        this.dataSource = dataSource;
    }
    
    public void save() {
        DataSourceContextHolder.setDataSource(dataSource);
        // ...
    }
}

问题:使用普通变量导致线程安全问题
解决:使用ThreadLocal保证线程隔离

十、最佳实践

1. 建议使用场景

  • 多租户系统(每个租户使用独立数据库)
  • 读写分离架构(主库MySQL,从库达梦)
  • 两地三中心架构(主库和灾备库)
  • 业务数据和日志数据分离存储

2. 不建议使用场景

  • 简单业务系统(增加复杂度不值得)
  • 单一业务场景(无需动态切换)
  • 高并发写入场景(需考虑锁机制)
  • 跨数据源的复杂业务逻辑(需分布式事务)

3. 推荐配置

spring:
  datasource:
    mysql:
      url: jdbc:mysql://localhost:3306/mysql_db?useSSL=false&serverTimezone=UTC&connectTimeout=30000
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        maximumPoolSize: 100
        idleTimeout: 30000
        poolName: mysql-pool
    dm:
      url: jdbc:dm://localhost:5236/dm_db?characterEncoding=utf8&connectTimeout=30000
      driver-class-name: dm.jdbc.driver.DmDriver
      hikari:
        maximumPoolSize: 50
        idleTimeout: 30000
        poolName: dm-pool

十一、总结

SpringBoot配置Mysql+达梦双数据源是实现多数据库访问的核心技术。通过AbstractRoutingDataSource抽象,结合AOP和注解,可以实现灵活的数据源切换。在实际应用中需要特别注意以下几点:

  1. 正确配置驱动类和连接参数,特别是达梦数据库的特殊要求
  2. 合理设计数据源切换策略,避免不必要的切换开销
  3. 配置合适的连接池参数,平衡资源利用率
  4. 处理好跨数据源的事务问题,避免数据不一致
  5. 注意线程安全和异常处理,确保系统稳定性

在实际项目中,应根据业务需求选择合适的实现方案。对于需要频繁切换数据源的场景,建议使用动态路由策略;对于固定切换的场景,可以直接配置多个数据源。同时,需要关注性能优化和安全风险,确保系统在高并发和复杂业务场景下的稳定性。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日