# scaffold 项目之动态数据源

# 简介

在一个项目中,免不了需要连接多个数据库来操作业务,比如做统计表,可能需要多个数据库的业务数据合并起来统计。本项目使用的是 MyBatis 框架,可以用它提供的 api 实现 动态数据源 还有 数据库连接池

# 数据库连接池

本项目数据连接池用的是 druid ,市面上常用的线程池,Druid 是阿里巴巴开源的高性能数据库连接池,广泛应用于 Java 应用中,支持多种数据库,并提供强大的监控、SQL 执行优化和安全功能。

# 功能特点

  • 性能优化:支持高并发访问,通过连接池管理减少数据库连接的频繁创建和销毁。
  • 监控功能:提供详细的监控页面,展示连接池状态、SQL 执行情况等。

  • SQL 防注入:内置防火墙功能,可以有效防止 SQL 注入攻击。

  • 扩展性:支持自定义拦截器和监控功能。

# 配置使用

  1. 引用依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
  2. 在项目中的 application.yml 文件中配置

    spring:
      datasource:
        druid: # Druid 【监控】相关的全局配置
          web-stat-filter:
            enabled: true
          stat-view-servlet:
            enabled: true
            allow: # 设置白名单,不填则允许所有访问
            url-pattern: /druid/*
            login-username: # 控制台管理用户名和密码
            login-password:
          filter:
            stat:
              enabled: true
              log-slow-sql: true # 慢 SQL 记录
              slow-sql-millis: 100
              merge-sql: true
            wall:
              config:
                multi-statement-allow: true

    注意: 这里只是配置了 druid 相关的配置,并没有配置多数据源。

    配置好后重新启动,访问项目的 http://127.0.0.1:8888/druid/index.html 就可以看到 druid 监控页面。

    相关参数说明请参阅 Druid 相关配置参数和说明 以获取更多详情。

# 多数据源

在项目中,我们可能会碰到需要多数据源的场景。例如说:

  • 读写分离:数据库主节点压力比较大,需要增加从节点提供读操作,以减少压力。
  • 多数据源:一个复杂的单体项目,因为没有拆分成不同的服务,需要连接多个业务的数据源。

本质上,读写分离,仅仅是多数据源的一个场景,从节点是只提供读操作的数据源。所以只要实现了多数据源的功能,也就能够提供读写分离。

# 多数据源方案

# 方案一

基于 Spring AbstractRoutingDataSource 做拓展

简单来说,通过继承 AbstractRoutingDataSource 抽象类,实现一个管理项目中多个 DataSource 的动态 DynamicRoutingDataSource 实现类。这样,Spring 在获取数据源时,可以通过 DynamicRoutingDataSource 返回实际的 DataSource 。

然后,我们可以自定义一个 @DS 注解,可以添加在 Service 方法、Dao 方法上,表示其实际对应的 DataSource 。

如此,整个过程就变成,执行数据操作时,通过 “配置” 的 @DS 注解,使用 DynamicRoutingDataSource 获得对应的实际的 DataSource 。之后,在通过该 DataSource 获得 Connection 连接,最后发起数据库操作。

不过呢,这种方式在结合 Spring 事务的时候,会存在无法切换数据源的问题。

目前实现比较好的是 baomidou 提供的 dynamic-datasource-spring-boot-starter

# 方案二

不同操作类,固定数据源

关于这个方案,解释起来略有点晦涩。以 MyBatis 举例子,假设有 ordersusers 两个数据源。 那么我们可以创建两个 SqlSessionTemplate ordersSqlSessionTemplateusersSqlSessionTemplate ,分别使用这两个数据源。

然后,配置不同的 Mapper 使用不同的 SqlSessionTemplate 。

如此,整个过程就变成,执行数据操作时,通过 Mapper 可以对应到其 SqlSessionTemplate ,使用 SqlSessionTemplate 获得对应的实际的 DataSource 。之后,在通过该 DataSource 获得 Connection 连接,最后发起数据库操作。

不过呢,这种方式在结合 Spring 事务的时候,也会存在无法切换数据源的问题。淡定淡定。多数据源的情况下,这个基本是逃不掉的问题。

# 方案三

分库分表中间件

对于分库分表的中间件,会解析我们编写的 SQL ,路由操作到对应的数据源。那么,它们天然就支持多数据源。如此,我们仅需配置好每个表对应的数据源,中间件就可以透明的实现多数据源或者读写分离。

目前,Java 最好用的分库分表中间件,就是 Apache ShardingSphere ,没有之一。

那么,这种方式在结合 Spring 事务的时候,会不会存在无法切换数据源的问题呢?答案是不会。在上述的方案一和方案二中,在 Spring 事务中,会获得对应的 DataSource ,再获得 Connection 进行数据库操作。而获得的 Connection 以及其上的事务,会通过 ThreadLocal 的方式和当前线程进行绑定。这样,就导致我们无法切换数据源。

难道分库分表中间件不也是需要 Connection 进行这些事情么?答案是的,但是不同的是分库分表中间件返回的 Connection 返回的实际是动态的 DynamicRoutingConnection ,它管理了整个请求(逻辑)过程中,使用的所有的 Connection ,而最终执行 SQL 的时候,DynamicRoutingConnection 会解析 SQL ,获得表对应的真正的 Connection 执行 SQL 操作。

难道方案一和方案二不可以这么做吗?答案是,当然可以。前提是,他们要实现解析 SQL 的能力。

那么,分库分表中间件就是多数据源的完美方案?从一定程度上来说,是的。但是,它需要解决多个 Connection 可能产生的多个事务的一致性问题,也就是我们常说的,分布式事务。

本项目使用的是第一种方案, 使用 dynamic-datasource-spring-boot-starter 来实现。 后续可以考虑集成使用第三种。

# 多数据源配置

  1. 引用 maven 依赖

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
    </dependency>
  2. 在项目中的 application.yml 文件中配置

    spring:
      # 数据源配置项
      autoconfigure:
        exclude:
          - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
      datasource:
        druid: # Druid 【监控】相关的全局配置
          web-stat-filter:
            enabled: true
          stat-view-servlet:
            enabled: true
            allow: # 设置白名单,不填则允许所有访问
            url-pattern: /druid/*
            login-username: # 控制台管理用户名和密码
            login-password:
          filter:
            stat:
              enabled: true
              log-slow-sql: true # 慢 SQL 记录
              slow-sql-millis: 100
              merge-sql: true
            wall:
              config:
                multi-statement-allow: true
        dynamic: # 多数据源配置
          druid: # Druid 【连接池】相关的全局配置
            initial-size: 1 # 初始连接数
            min-idle: 1 # 最小连接池数量
            max-active: 20 # 最大连接池数量
            max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
            time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
            min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
            max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
            validation-query: SELECT 1 # 配置检测连接是否有效
            test-while-idle: true
            test-on-borrow: false
            test-on-return: false
          primary: master
          datasource:
            master:
              name: scaffold
              url: jdbc:mysql://localhost:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&createDatabaseIfNotExist=true # MySQL Connector/J 
              username: root
              password: 123456
            slave: # 模拟从库,可根据自己需要修改
              name: scaffold_slave
              lazy: true # 开启懒加载,保证启动速度
              url: jdbc:mysql://localhost:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&createDatabaseIfNotExist=true # MySQL Connector/J 8.X 连接的示例
              username: root
              password: 123456

    解释:

    1. spring.autoconfigure.exclude: - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure 去除 Druid 自带的自动化配置。
    2. spring.datasource.druid.* Druid 连接池全局配置
    3. spring.datasource.dynamic.primary: master 指定主名为 master 的数据源, 可以换成其他的
    4. spring.datasource.dynamic.datasource.master.* 名为 master 的数据源的连接配置, 不一定是 master 这个名字
    5. spring.datasource.dynamic.datasource.slave.* 名为 slave 的数据源的连接配置,不一定是 slave 这个名字

# 数据源切换

可以使用 @DS 注解来切换对应的数据源,列如:

public class Service {
    
    @DS("master")
    public void aMethod () {
        
    }
    
    @DS("slave")
    public void bMethod() {
        
    }
    
}

这样在执行对应的方法就会使用不同的数据源。

# 事务相关

事务有 3 种解决方案,如下

  1. 单机 + 单机数据源: @Transactional 注解
  2. 单机 + 多数据源: @DSTransactional 注解
  3. 多机 + 单 / 多数据源: Seata 分布式事务

# @Transactional 注解

大多数情况下,是单机 + 单个数据源的操作,只需要在方法上添加 Spring @Transactional 注解,声明事务即可。

具体的使用,可以项目里搜 @Transactional 关键字,就可以看到非常多的使用示例。

# @DSTransactional 注解

如果单机 + 多个数据源的操作,使用 @Transactional 声明的事务中,无法进行数据源的切换。此时,可以使用 Dynamic Datasource 提供的 @DSTransactional 注解,支持多数据源的切换。

注意:

@Transactional 只是提供相对可靠的多数据源的事务一致性,并不是绝对的。

# 使用示例

有三个 Service 类, AService 调用 BServiceCService , 它们分别对应 a、b、c 三个数据源

public class AService {
    @Resource
    private BService bService;
    
    @Resource
    private CService cService;
    
    @DS("a")
    @DSTransactional
    public void create() {
        bService.create();
        cService.create();
    }
}
public class BService {
   
    
    @DS("b")
    public void create() {
		// 业务
    }
}
public class CService {
   
    
    @DS("c")
    public void create() {
		// 业务
    }
}

场景问题:

  1. 问题一

c 数据源的操作发生异常,a、b 数据源会回滚么?

  • 答案:会的,因为最终是最外层的 @DSTransactional 注解对应的方法结束后,才进行事务的提交,所以它实际不需要回滚。
  • 注意:但是有一点要注意,如果提交时某个数据源的事务提交失败,则可能导致事务的不一致。
  1. 问题二

b、c 数据源的方法,也添加 @DSTransactional 注解,会不会影响事务?

  • 答案:不会,可以放心大胆的加。

# 附录

# Druid 相关配置参数和说明

  1. web-stat-filter 配置

web-stat-filter 是用于监控 Web 应用的 SQL 执行情况的过滤器。

参数说明
enabled是否启用 WebStatFilter,默认为 false 。启用后可以监控 Web 应用的 SQL 执行情况。
url-pattern需要监控的 URL 模式,默认为 /* ,表示监控所有请求。
exclusions排除不需要监控的 URL,例如静态资源路径(如 /static/** )。
session-stat-max-count最大 session 统计数量,默认为 1000
session-stat-enable是否启用 session 统计,默认为 false
principal-session-name用于 session 统计的用户属性名,默认为 null
principal-cookie-name用于 session 统计的用户属性名,默认为 null
  1. stat-view-servlet 配置

stat-view-servlet 是 Druid 提供的监控页面,用于展示数据库连接池的状态和 SQL 执行情况。

参数说明
enabled是否启用 StatViewServlet,默认为 false 。启用后可以通过访问 /druid 查看监控页面。
url-pattern监控页面的访问路径,默认为 /druid/*
reset-enable是否允许重置数据,默认为 true 。如果设置为 false ,则不允许通过监控页面重置数据。
allow允许访问监控页面的 IP 白名单,多个 IP 用逗号分隔。如果不配置,则允许所有 IP 访问。
deny拒绝访问监控页面的 IP 黑名单,多个 IP 用逗号分隔。
login-username监控页面的登录用户名,默认为空。
login-password监控页面的登录密码,默认为空。
session-stat-max-count最大 session 统计数量,默认为 1000
session-stat-enable是否启用 session 统计,默认为 false
  1. filter 配置

filter 是用于配置 Druid 的过滤器,常见的过滤器包括 stat (SQL 统计)和 wall (SQL 防火墙)。

3.1 stat 配置

stat 过滤器用于统计 SQL 执行情况,包括慢 SQL 日志。

参数说明
enabled是否启用 SQL 统计过滤器,默认为 false
log-slow-sql是否记录慢 SQL,默认为 false 。启用后会将慢 SQL 打印到日志中。
slow-sql-millis慢 SQL 的阈值(单位:毫秒),默认为 1000 。如果 SQL 执行时间超过此值,则认为是慢 SQL。
merge-sql是否合并 SQL,默认为 false 。启用后会将多条 SQL 合并为一条显示。

3.2 wall 配置

wall 过滤器用于防止 SQL 注入攻击,提供 SQL 防火墙功能。

参数说明
enabled是否启用 SQL 防火墙,默认为 false
config配置 SQL 防火墙的规则。
multi-statement-allow是否允许执行多条 SQL 语句,默认为 false 。启用后允许通过分号分隔的多条 SQL 语句。