动态数据源

158 阅读15分钟

动态数据源

动态数据源允许应用程序在运行时,根据不同的业务需求切换到不同的数据库数据源。

一、基本概念

定义:动态数据源是指在应用程序运行过程中,能够根据不同的业务逻辑、用户请求或者其他条件动态地切换到不同的数据库连接,而不是在应用程序启动时就固定使用一个单一的数据源。

二、实现原理

基础知识:

  • 数据源抽象:SpringBoot 通过 DataSource 接口来抽象数据源。在动态数据源的实现中,会创建多个 DataSource 实例,每个实例对应一个不同的数据库。

  • 数据源上下文管理:通常会有一个专门的上下文管理器来维护数据源实例的引用,并且根据当前业务逻辑上下文来决定使用哪个数据源。

  • 拦截器或AOP切面:为了在业务逻辑执行过程中动态切换数据源,通常使用拦截器或 AOP 技术。

实操原理:

以 AbstractRoutingDataSource 实现方式原理图为基础,对如何实现动态数据源功能技术实现的核心点。

  • 自定义数据源管理:Spring 框架通过 DataSource 接口来标识数据源,因此实现动态数据源需要自定义实现对数据源的管理。
  • 存储数据源标识:通过动态数据源注解搭配AOP类来解析出当前需要使用的数据源标识(数据源名称),存储到数据源标识上下文或者其持有者中。用于在切换数据源时确定使用哪个数据源。
  • 数据源切换:在 AbstractRoutingDataSource 中获取连接时,会调用子类实现的 determineCurrentLookupKey 方法来确定数据源标识,再去数据源缓存中获取出对应的数据源返回。

因此,通过以上了解自定义实现动态数据源需要注意以下几点:

  • 自定义数据源管理:管理程序中所有数据源配置和数据源,包含初始化数据源、获取数据源等。
  • 存储当前使用的数据源标识:通过注解和 AOP 技术将注解上的所指定的数据源标识存储到数据源上下文中(或持有者中)。
  • 切换数据源:当 ORM 框架获取数据源连接时,从数据源上下文中获取数据源标识,再去数据源缓存中获取对应的数据源实例。
  • 多数据源事务问题。

三、实现方式

3.1、基于Spring提供的AbstractRoutingDataSource实现

知识:对动态数据源的支持1

实现原理图:

Spring 提供 AbstractRoutingDataSource 抽象类对多数据源的支持,允许根据某些条件动态选择和切换数据源。

3.1.1、第一步:动态数据源选择类

作用:确定当前需要使用哪个数据源,返回数据源标识即可。

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

import javax.sql.DataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 这里根据业务逻辑来确定当前的数据源
        return DataSourceContextHolder.getDataSourceType();
    }
}

3.1.2、第二步:当前使用数据源持有类

作用:存储当前请求使用的数据源。

public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSourceName(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static String getDataSourceName() {
        return contextHolder.get();
    }

    public static void clearDataSourceName() {
        contextHolder.remove();
    }
}

3.1.3、第三步:数据源切换

可使用 AOP 技术来动态切换数据源。使用动态数据源注解+AOP切换类来实现存储当前使用的数据源。

DynamicDataSource 注解用于标识使用哪个

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {
    // 数据源名称
	String name() default "";
}

DataSourceAspect 类用于解析 DynamicDataSource 注解并将数据源名称存储到 DataSourceContextHolder中。

注意:DataSourceAspect 是需要注入 Spring 的 IOC 容器中。

@Aspect
public class DataSourceAspect {

    /**
     * 多数据源环绕通知
     */
    @Around("cutService()")
    public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable {
        DataSource datasource = getDataSource(point);
        if (datasource != null) {
            DataSourceContextHolder.setDataSourceName(datasource.name());
        } else {
            // 默认使用主数据源
            DataSourceContextHolder.setDataSourceName(默认数据源);
        }
        log.debug("设置数据源为: {}", DataSourceContextHolder.getDataSourceName());

        try {
            return point.proceed();
        } finally {
            log.debug("清空数据源信息, 数据源: {}", DataSourceContextHolder.getDataSourceName());
            DataSourceContextHolder.clearDataSourceName();
        }
    }

    private String determineDataSource() {
        // 1.获取被拦截方法
        // 获取连接点签名,即被注解修饰的方法/类
        Signature signature = point.getSignature();
        MethodSignature methodSignature = null;
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        methodSignature = (MethodSignature) signature;
        // 获取连接点的目标对象,即被代理的对象(实际对象,而不是代理对象)
        Object target = point.getTarget();
        Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());

        // 2.获取方法上的DataSource注解
        DataSource curDataSource = currentMethod.getAnnotation(DataSource.class);
        return curDataSource.name();
    }
}

3.1.4、第四步:动态数据源的配置

在程序中需要切换多个数据源,需要配置多个数据源。将 DynamicDataSource 实例作为主数据源。

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource1() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/db1")
                .username("root")
                .password("root")
                .driverClassName("com.mysql.cj.jdbc.Driver")
                .build();
    }

    @Bean
    public DataSource dataSource2() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/db2")
                .username("root")
                .password("root")
                .driverClassName("com.mysql.cj.jdbc.Driver")
                .build();
    }

    @Bean
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        
        // 配置多个数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("dataSource1", dataSource1());
        targetDataSources.put("dataSource2", dataSource2());
        
        dynamicDataSource.setTargetDataSources(targetDataSources);
        
        // 配置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(dataSource1());
        
        return dynamicDataSource;
    }

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

}

3.1.5、优缺点

优点:

  • 可快速实现动态数据源功能。

缺点:

  • 无法动态添加和删除数据源:AbstractRoutingDataSource 抽象类继承了 InitializingBean接口,并在 afterPropertiesSet 方法内进行数据源的初始化,因此只能在 AbstractRoutingDataSource构建Bean 时初始化数据源。

3.2、自定义实现动态数据源(方案一)

AbstractRoutingDataSource 存在无法动态添加/删除数据源问题,但在实际使用中是需要支持在线动态增删改数据源连接信息。通过动态数据源实现原理​和基于Spring提供的AbstractRoutingDataSource实现​小节的了解,自定义实现动态数据源其实是对 Spring 提供 AbstractRoutingDataSource 抽象类的类似实现,以拓展其功能。

因此,本方案基于数据库表来实现动态数据源信息的加载及解析。

3.2.1、第一步:设计数据源表及DAO操作类

主数据库的数据源信息仍然配置在项目配置文件中,其余数据库的连接信息写入 sys_database_info 表中。

CREATE TABLE `sys_database_info`
(
    `db_id`             bigint       NOT NULL COMMENT '主键',
    `db_name`           varchar(255) NOT NULL COMMENT '数据库名称(英文名称)',
    `jdbc_driver`       varchar(255) NOT NULL COMMENT 'jdbc的驱动类型',
    `jdbc_url`          varchar(255) NOT NULL COMMENT 'jdbc的url',
    `username`          varchar(255) NOT NULL COMMENT '数据库连接的账号',
    `password`          varchar(255) NOT NULL COMMENT '数据库连接密码',
    `schema_name`       varchar(255) NULL DEFAULT NULL COMMENT '数据库的schema名称,每种数据库的schema意义都不同',
    `status_flag`       tinyint NULL DEFAULT NULL COMMENT '数据源状态:1-正常,2-无法连接',
    `error_description` varchar(500) NULL DEFAULT NULL COMMENT '连接失败原因',
    `remarks`           varchar(255) NULL DEFAULT NULL COMMENT '备注,摘要',
    `del_flag`          char(1)      NOT NULL DEFAULT 'N' COMMENT '是否删除,Y-被删除,N-未删除',
    `create_time`       datetime NULL DEFAULT NULL COMMENT '创建时间',
    `create_user`       bigint NULL DEFAULT NULL COMMENT '创建人',
    `update_time`       datetime NULL DEFAULT NULL COMMENT '修改时间',
    `update_user`       bigint NULL DEFAULT NULL COMMENT '修改人',
    PRIMARY KEY (`db_id`) USING BTREE
) COMMENT = '多数据源信息';

设计 DataBaseInfoPersistence 类用于读取 sys_database_info 表中数据源信息,初始化主数据源信息等。

  • createMasterDatabaseInfo方法:将主库的连接信息初始化到 sys_database_info 表中。
  • deleteMasterDatabaseInfo方法:清空 sys_database_info 表中 主库的连接信息。
  • getAllDataBaseInfo方法:获取 sys_database_info 表中所有数据库连接信息。
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 数据源信息(sys_database_info)相关操作的DAO
 */
@Slf4j
@RequiredArgsConstructor
public class DataBaseInfoPersistence {

    private final DbProp dbProp;

    /**
     * 初始化master的数据源,要和DbProp配置的数据源一致
     */
    public void createMasterDatabaseInfo() {
        Connection conn = null;
        try {
            Class.forName(dbProp.getDriverClassName());
            conn = DriverManager.getConnection(dbProp.getUrl(), dbProp.getUsername(), dbProp.getPassword());
            PreparedStatement preparedStatement = conn.prepareStatement(new AddDatabaseInfoSql().getSql(dbProp.getUrl()));

            // 设置记录属性
            preparedStatement.setLong(1, IdWorker.getId());
            preparedStatement.setString(2, "master");
            preparedStatement.setString(3, dbProp.getDriverClassName());
            preparedStatement.setString(4, dbProp.getUrl());
            preparedStatement.setString(5, dbProp.getUsername());
            preparedStatement.setString(6, dbProp.getPassword());
            preparedStatement.setString(7, "主数据源, 项目启动数据源");
            preparedStatement.setString(8, DateUtil.formatDateTime(new Date()));

            int saveNum = preparedStatement.executeUpdate();
            log.info("初始化master的databaseInfo信息!初始化" + saveNum + "条");
        } catch (Exception ex) {
            log.error("初始化master的databaseInfo信息错误!", ex);
            String userTip = StrUtil.format(DatasourceContainerExceptionEnum.INSERT_DBS_DAO_ERROR.getUserTip(), ex.getMessage());
            throw new DatasourceContainerException(DatasourceContainerExceptionEnum.INSERT_DBS_DAO_ERROR, userTip);
        } finally {
            IoUtil.close(conn);
        }
    }

    /**
     * 清空master数据源信息
     */
    public void deleteMasterDatabaseInfo() {
        Connection conn = null;
        try {
            // 加载驱动
            Class.forName(dbProp.getDriverClassName());
            conn = DriverManager.getConnection(dbProp.getUrl(), dbProp.getUsername(), dbProp.getPassword());
            PreparedStatement preparedStatement = conn.prepareStatement(new DeleteDatabaseInfoSql().getSql(dbProp.getUrl()));
            preparedStatement.setString(1, "master");
            int deleteNum = preparedStatement.executeUpdate();
            log.info("删除master的databaseInfo信息!删除" + deleteNum + "条");
        } catch (Exception ex) {
            log.info("删除master的databaseInfo信息失败", ex);
            throw new DatasourceContainerException(DatasourceContainerExceptionEnum.DELETE_DBS_DAO_ERROR, ex.getMessage());
        } finally {
            IoUtil.close(conn);
        }
    }

    public Map<String, DbProp> getAllDataBaseInfo() {
        Map<String, DbProp> dataSourceList = new HashMap<>(16);
        Connection conn = null;
        try {
            Class.forName(dbProp.getDriverClassName());
            conn = DriverManager.getConnection(dbProp.getUrl(), dbProp.getUsername(), dbProp.getPassword());
            PreparedStatement preparedStatement = conn.prepareStatement(new DatabaseListSql().getSql(dbProp.getUrl()));
            ResultSet resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                DbProp newDbProp = this.createDbProp(resultSet);
                String dbName = resultSet.getString("db_name");
                dataSourceList.put(dbName, newDbProp);
            }

            return dataSourceList;
        } catch (Exception ex) {
            log.error("查询数据源信息错误!", ex);
            String userTip = StrUtil.format(DatasourceContainerExceptionEnum.QUERY_DBS_DAO_ERROR.getUserTip(), ex.getMessage());
            throw new DatasourceContainerException(DatasourceContainerExceptionEnum.QUERY_DBS_DAO_ERROR, userTip);
        } finally {
            IoUtil.close(conn);
        }
    }

    /**
     * 通过查询结果组装DbProp
     */
    private DbProp createDbProp(ResultSet resultSet) {
        DbProp newDbProp = new DbProp();
        BeanUtil.copyProperties(this.dbProp, newDbProp, CopyOptions.create().ignoreError());

        try {
            newDbProp.setDriverClassName(resultSet.getString("jdbc_driver"));
            newDbProp.setUrl(resultSet.getString("jdbc_url"));
            newDbProp.setUsername(resultSet.getString("username"));
            newDbProp.setPassword(resultSet.getString("password"));
        } catch (Exception ex) {
            log.info("根据数据库查询结果,创建DruidProperties失败", ex);
            String userTip = StrUtil.format(DatasourceContainerExceptionEnum.CREATE_PROP_DAO_ERROR.getUserTip(), ex.getMessage());
            throw new DatasourceContainerException(DatasourceContainerExceptionEnum.CREATE_PROP_DAO_ERROR, userTip);
        }

        return newDbProp;
    }

}

对 sys_database_info 表增删查的语句类。

数据库类型枚举,列举出系统所支持的数据类型。

public enum DbTypeEnum {

    /**
     * MySQL
     */
    MYSQL("jdbc:mysql", "mysql", "select 1"),

    /**
     * PostgreSQL
     */
    PG_SQL("jdbc:postgresql", "pgsql", "select version()"),
    ;

    /**
     * spring.datasource.url中包含的关键字
     */
    private final String urlWords;

    /**
     * mapping.xml使用databaseId="xxx"来标识的关键字
     */
    private final String xmlDatabaseId;

    /**
     * validateQuery所使用的语句
     */
    private final String connectionTestQuery;

    DbTypeEnum(String urlWords, String xmlDatabaseId, String validateQuery) {
        this.urlWords = urlWords;
        this.xmlDatabaseId = xmlDatabaseId;
        this.connectionTestQuery = validateQuery;
    }

    /**
     * 通过数据库连接的URL判断是哪种数据库
     *
     * @param url 数据库连接的URL
     * @return 枚举名称,如MYSQL
     */
    public static String getTypeByUrl(String url) {
        if (url == null) {
            return MYSQL.name();
        }

        for (DbTypeEnum value : DbTypeEnum.values()) {
            if (url.contains(value.getUrlWords())) {
                return value.name();
            }
        }

        return MYSQL.name();
    }

}

AbstractSql 是获取操作数据库表的抽象类。

public abstract class AbstractSql {

    public String getSql(String jdbcUrl) {
        if (jdbcUrl.contains(DbTypeEnum.MYSQL.getUrlWords())) {
            return mysql();
        }
        if (jdbcUrl.contains(DbTypeEnum.PG_SQL.getUrlWords())) {
            return pgSql();
        }
        return mysql();
    }

    /**
     * 获取MySQL的SQL语句
     */
    protected abstract String mysql();

    /**
     * 获取PgSQL的SQL语句
     */
    protected abstract String pgSql();

}

以下是操作语句类。

public class AddDatabaseInfoSql extends AbstractSql {

    @Override
    protected String mysql() {
        return "INSERT INTO `sys_database_info`(`db_id`, `db_name`, `jdbc_driver`, `jdbc_url`, `username`, `password`, `remarks`, `create_time`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
    }

    @Override
    protected String pgSql() {
        return "INSERT INTO sys_database_info(db_id, db_name, jdbc_driver, jdbc_url, username, password,  remarks, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, to_timestamp(?,'YYYY-MM-DD HH24:MI:SS'))";
    }

}

public class DatabaseListSql extends AbstractSql {

    @Override
    protected String mysql() {
        return "select db_name, jdbc_driver, jdbc_url, username, password from sys_database_info where del_flag = 'N'";
    }

    @Override
    protected String pgSql() {
        return "select db_name,jdbc_driver,jdbc_url,username,password from sys_database_info";
    }

}

public class DeleteDatabaseInfoSql extends AbstractSql {

    @Override
    protected String mysql() {
        return "DELETE from sys_database_info where db_name = ?";
    }

    @Override
    protected String pgSql() {
        return "DELETE from sys_database_info where db_name = ?";
    }

}

3.2.2、第二步:数据库配置类

DbProp 类用于存储数据库连接信息,用于创建数据源时使用。

@Data
@Slf4j
public class DbProp {

    /**
     * 数据源名称
     */
    private String dataSourceName;

    /**
     * 数据库驱动名称
     */
    private String driverClassName;

    /**
     * 连接数据库的URL
     * <p>如MySQL: jdbc:mysql://127.0.0.1:3306/bens</p>
     */
    private String url;

    /**
     * 连接数据库的用户名
     */
    private String username;

    /**
     * 连接数据库的密码
     */
    private String password;

    /**
     * 连接池初始化大小
     * 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
     */
    private Integer initialSize = 2;

    /**
     * 最大连接池数量
     */
    private Integer maxActive = 20;

    /**
     * 最小连接池数量
     */
    private Integer minIdle = 1;

    /**
     * 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
     */
    private Integer maxWait = 60000;

    /**
     * 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
     */
    private Boolean poolPreparedStatements = true;

    /**
     * 要启用PSCache,必须配置大于0,可以配置-1关闭
     * 当大于0时,poolPreparedStatements自动触发修改为true。
     */
    private Integer maxPoolPreparedStatementPerConnectionSize = 100;

    /**
     * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。
     * 如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
     */
    private String validationQuery;

    /**
     * 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
     */
    private Integer validationQueryTimeout = 10;

    /**
     * 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
     */
    private Boolean testOnBorrow = true;

    /**
     * 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
     */
    private Boolean testOnReturn = true;

    /**
     * 建议配置为true,不影响性能,并且保证安全性。
     * 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
     */
    private Boolean testWhileIdle = true;

    /**
     * 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
     */
    private Boolean keepAlive = false;

    /**
     * 有两个含义:
     * 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
     * 2) testWhileIdle 的判断依据,详细看 testWhileIdle 属性的说明
     */
    private Integer timeBetweenEvictionRunsMillis = 60000;

    /**
     * 连接保持空闲而不被驱逐的最小时间
     */
    private Integer minEvictableIdleTimeMillis = 300000;

    /**
     * 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
     * 监控统计用的filter:stat
     * 日志用的filter:log4j
     * 防御sql注入的filter:wall
     */
    private String filters = "stat";

}

3.2.3、第三步:当前使用的数据源上下文

定义 CurrentDataSourceContext 类存储当前请求所使用的数据源,用于获取数据源标识。使用 ThreadLocal 线程本地变量存储数据源标识。

public class CurrentDataSourceContext {

    /**
     * 当前数据源名称
     */
    private static final ThreadLocal<String> DATASOURCE_CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceName(String dataSourceName) {
        DATASOURCE_CONTEXT_HOLDER.set(dataSourceName);
    }

    public static String getDataSourceName() {
        return DATASOURCE_CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceName() {
        DATASOURCE_CONTEXT_HOLDER.remove();
    }

}

3.2.4、第四步:动态数据源注解及AOP

定义 @DataSource 注解用于标识业务方法/类使用的数据源。

@Inherited // 允许子类继承
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    /**
     * 数据源名称
     */
    String name() default "";

}

MultiSourceExchangeAop 类利用 Spring AOP 技术拦截 @DataSource 注解获取业务使用的数据源标识,并且将数据源标识存储到当前使用的数据源上下文​中。需要将此 AOP 实现类注入到 Spring IOC容器中。

@Slf4j
@Aspect
public class MultiSourceExchangeAop implements Ordered {

    @Pointcut(value = "@annotation(cn.ibenbeni.bens.ds.api.annotation.DataSource)")
    private void cutService() {
    }

    /**
     * 多数据源环绕通知
     */
    @Around("cutService()")
    public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable {
        DataSource datasource = getDataSource(point);
        if (datasource != null) {
            CurrentDataSourceContext.setDataSourceName(datasource.name());
        } else {
            // 默认使用主数据源
            CurrentDataSourceContext.setDataSourceName(主数据源);
        }
        log.debug("设置数据源为: {}", CurrentDataSourceContext.getDataSourceName());

        try {
            return point.proceed();
        } finally {
            log.debug("清空数据源信息, 数据源: {}", CurrentDataSourceContext.getDataSourceName());
            CurrentDataSourceContext.clearDataSourceName();
        }
    }

    /**
     * 获取数据源名称
     */
    private static DataSource getDataSource(ProceedingJoinPoint point) throws NoSuchMethodException {
        // 1.获取被拦截方法
        // 获取连接点签名,即被注解修饰的方法/类
        Signature signature = point.getSignature();
        MethodSignature methodSignature = null;
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        methodSignature = (MethodSignature) signature;
        // 获取连接点的目标对象,即被代理的对象(实际对象,而不是代理对象)
        Object target = point.getTarget();
        Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());

        // 2.获取方法上的DataSource注解
        return currentMethod.getAnnotation(DataSource.class);
    }

    /**
     * AOP的顺序要早于Spring的事务
     */
    @Override
    public int getOrder() {
        return 1;
    }

}

3.2.5、第五步、自定义数据源管理

3.2.5.1、动态数据源存储容器及工厂类

定义 DataSourceContext 类用于缓存数据源实例和数据库连接配置实例,并且提供初始化数据源能力。DATA_SOURCES 和 DATA_SOURCES_CONF 属性类型为 Map集合分别缓存缓存数据源实例和数据库连接配置实例。

initDataSource 方法提供初始化动态数据源的能力,主要步骤:

  1. 根据配置文件中主库连接信息初始化,因此先清空数据库信息,再初始化主库连接信息到 sys_database_info 表中。
  2. 获取 sys_database_info 表中所有数据库连接信息,利用工厂类根据 DbProp 数据库连接信息对 DataSource 进行初始化。
  3. 将初始化完成的 DataSource 实例及数据库连接配置实例缓存到 DATA_SOURCES 和 DATA_SOURCES_CONF 属性中。
import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// 动态数据源存储容器
public class DataSourceContext {

    /**
     * 数据源容器
     * <p>key=数据源名称;value=数据源实例</p>
     */
    private static final Map<String, DataSource> DATA_SOURCES = new ConcurrentHashMap<>();

    /**
     * 数据源配置容器
     * <p>key=数据源名称;value=数据源配置实例</p>
     */
    private static Map<String, DbProp> DATA_SOURCES_CONF = new ConcurrentHashMap<>();

    /**
     * 初始化数据源
     *
     * @param dbProp     数据源配置实例
     * @param dataSource 数据源实例
     */
    public static void initDataSource(DbProp dbProp, DataSource dataSource) {
        // 清空数据库中的主数据源信息
        new DataBaseInfoPersistence(dbProp).deleteMasterDatabaseInfo();
        // 初始化主数据源信息
        new DataBaseInfoPersistence(dbProp).createMasterDatabaseInfo();

        // 从数据库中获取所有数据源信息
        DataBaseInfoPersistence dataBaseInfoDao = new DataBaseInfoPersistence(dbProp);
        Map<String, DbProp> allDataBaseInfo = dataBaseInfoDao.getAllDataBaseInfo();

        // 赋给全局变量
        DATA_SOURCES_CONF = allDataBaseInfo;

        // 根据数据源配置初始化数据源实例
        for (Map.Entry<String, DbProp> entry : allDataBaseInfo.entrySet()) {
            String dbName = entry.getKey();
            DbProp newDbProp = entry.getValue();

            // 若是主数据源,不用初始化第二次;否则初始化
            if (dbName.equalsIgnoreCase("master")) {
                DATA_SOURCES_CONF.put(dbName, newDbProp);
                DATA_SOURCES.put(dbName, dataSource);
            } else {
                DataSource newDataSource = createDataSource(dbName, newDbProp);
                DATA_SOURCES.put(dbName, newDataSource);
            }
        }
    }

    /**
     * 获取所有数据源
     */
    public static Map<String, DataSource> getDataSources() {
        return DATA_SOURCES;
    }

    /**
     * 创建数据源
     *
     * @param dataSourceName 数据源名称
     * @param dbProp         数据源配置
     */
    private static DataSource createDataSource(String dataSourceName, DbProp dbProp) {
        DATA_SOURCES_CONF.put(dataSourceName, dbProp);
        return DatasourceFactory.createDatasource(dbProp);
    }

}

利用工厂类来根据 DbProp 数据连接配置来初始化 DataSource 实例。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import cn.hutool.core.util.ObjectUtil;
import javax.sql.DataSource;

@Slf4j
public class DatasourceFactory {

    /**
     * 获取数据源
     *
     * @param dbProp 数据库配置
     */
    public static DataSource createDatasource(DbProp dbProp) {
        // 设置数据库连接信息
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(dbProp.getDriverClassName());
        hikariConfig.setJdbcUrl(dbProp.getUrl());
        hikariConfig.setUsername(dbProp.getUsername());
        hikariConfig.setPassword(dbProp.getPassword());
        hikariConfig.setMaximumPoolSize(dbProp.getMaxActive());
        hikariConfig.setMinimumIdle(dbProp.getMinIdle());
        hikariConfig.setConnectionTimeout(dbProp.getMaxWait());
        hikariConfig.addDataSourceProperty("cachePrepStmts", dbProp.getPoolPreparedStatements());
        hikariConfig.addDataSourceProperty("prepStmtCacheSize", dbProp.getMaxPoolPreparedStatementPerConnectionSize());
        hikariConfig.setConnectionTestQuery(getConnectionTestQueryByUrl(dbProp.getUrl()));
        hikariConfig.setValidationTimeout(dbProp.getValidationQueryTimeout() * 1000);
        if (ObjectUtil.isNotEmpty(dbProp.getKeepAlive()) && dbProp.getKeepAlive()) {
            if (dbProp.getKeepAlive() == false) {
                hikariConfig.setKeepaliveTime(0);
            } else {
                hikariConfig.setKeepaliveTime(dbProp.getTimeBetweenEvictionRunsMillis());
            }
        }

        return new HikariDataSource(hikariConfig);
    }

    /**
     * 获取数据库连接测试查询语句
     */
    private static String getConnectionTestQueryByUrl(String url) {
        for (DbTypeEnum value : DbTypeEnum.values()) {
            if (url.contains(value.getUrlWords())) {
                return value.getConnectionTestQuery();
            }
        }

        // 默认MySQL数据库
        return DbTypeEnum.MYSQL.getConnectionTestQuery();
    }

}
3.2.5.2、初始化数据源

初始化数据源时机是 ApplicationContext(Spring 上下文) 初始化完成,但未加载任何 Bean 定义时触发,即 Spring 上下文初始化完成, 意味着 Bean 配置类被加载、初始化完成并准备好使用。

监听 ApplicationContext(Spring 上下文) 初始化完成事件的类是 ApplicationContextInitializedEvent,因此需要实现 ApplicationListener接口并且泛型为 ApplicationContextInitializedEvent。在此提供 Context 初始化完成监听抽象类名称为 ContextInitializedListener。

import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@Slf4j
public abstract class ContextInitializedListener implements ApplicationListener<ApplicationContextInitializedEvent> {

    @Override
    public void onApplicationEvent(ApplicationContextInitializedEvent event) {
        // 若是注解配置上下文,则忽略
        ConfigurableApplicationContext applicationContext = event.getApplicationContext();
        if (applicationContext instanceof AnnotationConfigApplicationContext) {
            return;
        }

        // 执行具体业务逻辑
        this.eventCallback(event);
    }

    /**
     * 监听器具体的业务逻辑
     */
    public abstract void eventCallback(ApplicationContextInitializedEvent event);

}

通过继承并实现 ContextInitializedListener 监听器抽象类,在 Spring 上下文初始化完成后,对数据源进行初始化并且读取主库的连接信息,并且创建主数据库数据源。根据主库中 sys_database_info 表数据库连接信息进行数据源初始化,实际初始化是调用DataSourceContext.initDataSource​完成。

@Slf4j
public class DataSourceInitListener extends ContextInitializedListener implements Ordered {

    @Override
    public void eventCallback(ApplicationContextInitializedEvent event) {
        ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();

        // 获取数据库配置实例
        DbProp dbProp = this.getDbProp(environment);
        // 创建数据源
        DataSource datasource = DatasourceFactory.createDatasource(dbProp);

        try {
            // 初始化数据源容器
            DataSourceContext.initDataSource(dbProp, datasource);
        } catch (Exception ex) {
            log.error("初始化数据源容器错误", ex);
            throw new DatasourceContainerException(DatasourceContainerExceptionEnum.INIT_DATASOURCE_CONTAINER_ERROR, ex.getMessage());
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 200;
    }

    /**
     * 获取数据库配置实例
     *
     * @param environment 程序配置信息
     */
    private DbProp getDbProp(ConfigurableEnvironment environment) {
        DbProp dbProp = new DbProp();

        // 获取数据库连接配置
        String dataSourceDriver = environment.getProperty("spring.datasource.driver-class-name");
        String dataSourceUrl = environment.getProperty("spring.datasource.url");
        String dataSourceUsername = environment.getProperty("spring.datasource.username");
        String dataSourcePassword = environment.getProperty("spring.datasource.password");
        String initialSize = environment.getProperty("spring.datasource.initialSize");
        String maxActive = environment.getProperty("spring.datasource.maxActive");
        String minIdle = environment.getProperty("spring.datasource.minIdle");
        String maxWait = environment.getProperty("spring.datasource.maxWait");
        String poolPreparedStatements = environment.getProperty("spring.datasource.poolPreparedStatements");
        String maxPoolPreparedStatementPerConnectionSize = environment.getProperty("spring.datasource.maxPoolPreparedStatementPerConnectionSize");
        String validationQuery = environment.getProperty("spring.datasource.validationQuery");
        String validationQueryTimeout = environment.getProperty("spring.datasource.validationQueryTimeout");
        String testOnBorrow = environment.getProperty("spring.datasource.testOnBorrow");
        String testOnReturn = environment.getProperty("spring.datasource.testOnReturn");
        String testWhileIdle = environment.getProperty("spring.datasource.testWhileIdle");
        String keepAlive = environment.getProperty("spring.datasource.keepAlive");
        String timeBetweenEvictionRunsMillis = environment.getProperty("spring.datasource.timeBetweenEvictionRunsMillis");
        String minEvictableIdleTimeMillis = environment.getProperty("spring.datasource.minEvictableIdleTimeMillis");
        String filters = environment.getProperty("spring.datasource.filters");

        // 数据源重要连接信息不能为空
        if (StrUtil.hasBlank(dataSourceDriver, dataSourceUrl, dataSourceUsername, dataSourcePassword)) {
            String userTip = StrUtil.format(DatasourceContainerExceptionEnum.DB_CONNECTION_INFO_EMPTY_ERROR.getUserTip(), dataSourceUrl, dataSourceUsername);
            throw new DatasourceContainerException(DatasourceContainerExceptionEnum.DB_CONNECTION_INFO_EMPTY_ERROR, userTip);
        }

        // 创建数据源配置实体
        dbProp.setDriverClassName(dataSourceDriver);
        dbProp.setUrl(dataSourceUrl);
        dbProp.setUsername(dataSourceUsername);
        dbProp.setPassword(dataSourcePassword);
        if (StrUtil.isNumeric(initialSize)) {
            dbProp.setInitialSize(Convert.toInt(initialSize));
        }
        if (StrUtil.isNumeric(maxActive)) {
            dbProp.setMaxActive(Convert.toInt(maxActive));
        }
        if (StrUtil.isNumeric(minIdle)) {
            dbProp.setMinIdle(Convert.toInt(minIdle));
        }
        if (StrUtil.isNumeric(maxWait)) {
            dbProp.setMaxWait(Convert.toInt(maxWait));
        }
        if (ObjectUtil.isNotEmpty(poolPreparedStatements)) {
            dbProp.setPoolPreparedStatements(Convert.toBool(poolPreparedStatements));
        }
        if (StrUtil.isNumeric(maxPoolPreparedStatementPerConnectionSize)) {
            dbProp.setMaxPoolPreparedStatementPerConnectionSize(Convert.toInt(maxPoolPreparedStatementPerConnectionSize));
        }
        if (ObjectUtil.isNotEmpty(validationQuery)) {
            dbProp.setValidationQuery(validationQuery);
        }
        if (StrUtil.isNumeric(validationQueryTimeout)) {
            dbProp.setValidationQueryTimeout(Convert.toInt(validationQueryTimeout));
        }
        if (ObjectUtil.isNotEmpty(testOnBorrow)) {
            dbProp.setTestOnBorrow(Convert.toBool(testOnBorrow));
        }
        if (ObjectUtil.isNotEmpty(testOnReturn)) {
            dbProp.setTestOnReturn(Convert.toBool(testOnReturn));
        }
        if (ObjectUtil.isNotEmpty(testWhileIdle)) {
            dbProp.setTestWhileIdle(Convert.toBool(testWhileIdle));
        }
        if (ObjectUtil.isNotEmpty(keepAlive)) {
            dbProp.setKeepAlive(Convert.toBool(keepAlive));
        }
        if (StrUtil.isNumeric(timeBetweenEvictionRunsMillis)) {
            dbProp.setTimeBetweenEvictionRunsMillis(Convert.toInt(timeBetweenEvictionRunsMillis));
        }
        if (StrUtil.isNumeric(minEvictableIdleTimeMillis)) {
            dbProp.setMinEvictableIdleTimeMillis(Convert.toInt(minEvictableIdleTimeMillis));
        }
        if (ObjectUtil.isNotEmpty(filters)) {
            dbProp.setFilters(filters);
        }

        return dbProp;
    }

}

3.2.6、第六步:数据源切换

自定义数据源管理实现类需要实现 DataSource接口,Spring 提供了 AbstractDataSource 抽象类为 DataSource接口的基础抽象类,因此继承 AbstractDataSource 抽象类。

AbstractRoutingDataSource 类为 AbstractDataSource 抽象类的实现类,作为抽象动态数据源类,提供获取连接方法、获取 DataSource原始实例等功能,并且定义 determineDataSource方法用于获取当前使用数据源实例。

ORM 框架会调用 DataSource 接口的 getConnection方法获取数据库连接,并且 AbstractRoutingDataSource类实现了 DataSource 接口。因此,会实际调用 AbstractRoutingDataSource 抽象类的 getConnection方法。

import org.springframework.jdbc.datasource.AbstractDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    /**
     * 子类实现决定最终数据源
     *
     * @return 数据源
     */
    protected abstract DataSource determineDataSource();

    /**
     * 获取数据库连接
     *
     * @return 数据库连接对象
     */
    @Override
    public Connection getConnection() throws SQLException {
        return determineDataSource().getConnection();
    }

    /**
     * 获取数据库连接
     *
     * @param username 用户账号
     * @param password 用户密码
     * @return 数据库连接对象
     */
    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineDataSource().getConnection(username, password);
    }

    /**
     * 获取JDBC驱动原始类型实例
     *
     * @param iface 返回对象必须实现的Class
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> iface) throws SQLException {
        if (iface.isInstance(this)) {
            return (T) this;
        }
        return determineDataSource().unwrap(iface);
    }

    /**
     * 判断当前对象是否是指定接口的实现类
     *
     * @param iface 接口的Class
     */
    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));
    }

}

DynamicDataSource 类是 AbstractRoutingDataSource 类的实现类,实现其 determineDataSource核心方法返回当前使用数据源实例,即从当前 CurrentDataSourceContext(当前使用数据源上下文)中获取使用数据源标识,再从 DataSourceContext(数据源上下文)中根据数据源标识获取数据源实例,若为空,则默认返回主库的数据源实例。

import cn.hutool.core.util.StrUtil;
import javax.sql.DataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected DataSource determineDataSource() {
        // 获取当前数据源名称
        String dataSourceName = CurrentDataSourceContext.getDataSourceName();

        // 若无值,则默认使用主数据源
        if (StrUtil.isBlank(dataSourceName)) {
            dataSourceName = DatasourceContainerConstants.MASTER_DATASOURCE_NAME;
        }

        // 获取对应的数据源
        return DataSourceContext.getDataSources().get(dataSourceName);
    }

}

3.2.7、优缺点

优点:

  • 解决了基于 Spring 的 AbstractRoutingDataSource 不能动态增删数据源。

缺点:

  • 多数据源与数据源能力耦合,且无法扩展。
  • 仅适配 HikariCP 连接池,其余连接池无法支持。
  • 动态数据源存储容器(DataSourceContext)未提供程序动态增删能力。

四、应用场景

多租户系统:在多租户系统中,不同租户可能会使用不同的数据库来存储数据,通过动态数据源可以根据租户的身份信息动态切换到相应的数据源,从而实现租户数据的隔离和独立访问。

读写分离:为了提高数据库的读写性能,通常采用读写分离架构,即一个主数据库用于处理写操作,多个从数据库用于处理读操作。动态数据源可以根据业务逻辑的读写需求动态切换到主数据库或从数据库,实现读写分离的效果。

分库分表:当数据量非常大时,可能会采用分库分表的方式来存储数据。动态数据源可以根据业务逻辑的规则(如根据数据的业务类型、时间范围等)动态切换到不同的数据库或表,从而实现数据的分库分表访问。

数据迁移:在进行数据库迁移时,可能需要同时访问新旧两个数据库,动态数据源可以方便地在新旧数据库之间切换,实现数据的同步和迁移操作。

五、注意事项

5.1、多数据源的AOP切面需早于Spring事务切面执行

多数据源的AOP切面需要早于Spring的事务切面执行,有以下原因:

事务依赖数据源

Spring的事务管理是基于数据源的,事务的开启、提交和回滚操作都需要依赖于当前的数据源。如果在事务开始之前没有正确切换到目标数据源,事务管理器将无法正确地对目标数据源进行事务操作,从而导致事务失效或数据不一致的问题。默认情况下,Spring的事务切面的优先级是最低的(Ordered.LOWEST_PRECEDENCE)。

DataSource接口:数据源(连接池),提供数据库连接,并支持数据库连接池。

AbstractDataSource抽象类:DataSource接口抽象基础模板,可创建自定义的数据源类。

unwrap 方法:

public <T> T unwrap(Class<T> iface) throws SQLException;

作用:用于返回 DataSource 的底层实现。通过它可以解包​出 DataSource 的原始类型(如果它是被代理或者包装的)。通常用于将代理对象转化为目标类型。

参数:

  • iface:想要解包的预期Class,如果当前对象是该类型的包装器,unwrap 会返回目标类型的实例;如果当前对象不能被解包为该类型,它将抛出SQLException。

返回值:返回目标类型的实例。

isWrapperFor 方法:

public boolean isWrapperFor(Class<?> iface) throws SQLException;

作用:用于检查当前对象是否是指定类型的包装器。它是用来判断当前对象是否包装了目标类型的实例。

参数:

  • iface:想要检查的目标类型。

返回值:如果当前对象是指定类型的包装器,则返回 true;否则返回 false。

AbstractDataSource类

继承AbstractDataSource为什么实现unwrap和isWrapperFor方法呢?

继承AbstractDataSource并实现unwrap和isWrapperFor方法的主要原因是遵循JDBC规范,支持Wrapper机制,提供默认实现以简化子类的实现,并确保与现有工具和框架的兼容性。通过实现这些方法,你的数据源类可以提供更灵活的配置和管理选项,同时确保与JDBC工具和框架的兼容性。

Footnotes

  1. 对动态数据源的支持

    一、基本介绍

    AbstractRoutingDataSource 是 Spring 提供的抽象类,用于实现动态数据源的切换。它继承自 AbstractDataSource 类,并实现了 DataSource 接口。该类允许根据某些条件动态选择和切换数据源。

    AbstractRoutingDataSource 类主要通过 determineCurrentLookupKey()方法动态选择数据源,并通过 setTargetDataSources()方法来配置多个数据源,可根据请求上下文中某个属性来切换适合的数据源。

    二、方法

    determineCurrentLookupKey():获取当前要适应的数据源的标识

    determineCurrentLookupKey()方法是 AbstractRoutingDataSource 抽象类的核心方法,该方法返回一个标识(如字符串),用来标识当前需要使用的数据源。

    setTargetDataSources(Map<Object, Object> targetDataSources):设置数据源集合

    用于设置多个数据源,targetDataSources 是一个 Map集合,其 key=数据源标识,vlaue=数据源实例。该方法在配置类中调用给 AbstractRoutingDataSource 提供多个数据源。

    setDefaultTargetDataSource(Object defaultTargetDataSource):

    设置默认数据源,当 determineCurrentLookupKey()方法没有返回有效的数据源标识时,Spring 会使用默认数据源。