小小多数据源,拿下拿下

前言

先了解一波什么是数据源,这里引用百度文库上的一句

数据源是指数据库应用程序所使用的数据库或者数据库服务器。

数据源(Data Source)顾名思义,数据的来源,是提供某种所需要数据的器件或原始媒体。在数据源中存储了所有建立数据库连接的信息。就像通过指定文件名称可以在文件系统中找到文件一样,通过提供正确的数据源名称,你可以找到相应的数据库连接。

关键信息:

数据源存储了数据库的连接信息,比如以下:

java 复制代码
 spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/school?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useInformationSchema=true
      username: root
      password: root
     。。。。。

这是在springboot中的配置,我们这里是配置了一条数据源,如果我们配置了多条这样的配置信息,是不是就意味着我们配置了多数据源,以下将介绍一下多数据源实现原理

环境准备

准备两个库

库1

sql 复制代码
CREATE DATABASE `db_datasource1` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户ID',
  `real_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户真实姓名',
  `dept_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门ID',
  `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户邮箱',
  `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码',
  `sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在1代表删除)',
  `create_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系统用户' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;


INSERT INTO `sys_user` VALUES ('1', 'Shen Jiehong', 'HImSjOldFD', 'shenj@yahoo.com', '74-290-4854', 'M', 'U', 'N', 'bAGDP4vz24', '2009-12-01 03:20:27', '2013-11-20', '2007-03-12 21:15:15', 'HjcojsEOef');
INSERT INTO `sys_user` VALUES ('10', 'Nomura Miu', 'FWZBmk4gLG', 'nomi@yahoo.com', '5642 551482', 'F', 'W', 'Q', 'JdQBb9AGyI', '2019-09-30 23:32:32', '2022-04-11', '2019-03-27 12:10:22', 'uAdHDe9ebf');
INSERT INTO `sys_user` VALUES ('2', 'Tiffany Foster', 'ZVZIq3mPf4', 'fostertiffany720@mail.com', '66-540-0361', 'F', 'Z', 'a', 'G6spXmocKm', '2005-12-11 10:04:44', '2003-04-19', '2018-08-18 07:03:37', 'YhXitiLmCt');
INSERT INTO `sys_user` VALUES ('3', 'Kimura Hazuki', 'n6o04Cdb5x', 'kimurah@hotmail.com', '90-8458-7963', 'F', 'i', 'L', 'R84R75eyiQ', '2020-03-21 06:56:00', '2016-12-17', '2007-11-19 13:17:45', 'qYfpPhJ4RE');
INSERT INTO `sys_user` VALUES ('4', 'Yue Cho Yee', 'AMHgUb0Yw5', 'yuchoye43@icloud.com', '(1223) 65 1256', 'F', '2', '4', 'yxzrxQKz7D', '2005-06-04 12:41:34', '2000-04-07', '2008-11-06 17:24:53', 'NbR0NvXgMD');
INSERT INTO `sys_user` VALUES ('5', 'Amy Fernandez', 'vKctPEe7GV', 'amyfernandez1963@gmail.com', '196-6038-4330', 'F', 'f', 't', 'C9nTVCS4ca', '2016-03-09 03:59:15', '2010-08-22', '2002-10-26 02:00:18', 'Bs0pNTize3');
INSERT INTO `sys_user` VALUES ('6', 'Lam Tak Wah', 'eZUOFVEO3a', 'lam5@outlook.com', '718-717-3358', 'M', 'm', '3', 'GtebIinCra', '2015-05-09 07:47:31', '2004-07-27', '2012-02-14 23:31:22', 'BPYtx8BUE3');
INSERT INTO `sys_user` VALUES ('7', 'Xiao Yuning', 'lJ3xQTGA8Q', 'yuning5@icloud.com', '330-570-1809', 'M', 'k', 'T', 'EVN6XyuSkz', '2010-08-03 12:03:36', '2008-02-15', '2016-04-17 04:20:27', '9tugzv8rJz');
INSERT INTO `sys_user` VALUES ('8', 'Xu Jialun', 'WzRAV3kWYU', 'xujialun915@hotmail.com', '(116) 809 8039', 'M', 'V', 't', 'NMqT5Us1Ey', '2003-07-15 07:23:11', '2006-09-09', '2014-06-07 10:18:13', '1crvm0kfMk');
INSERT INTO `sys_user` VALUES ('9', 'Kojima Momoe', 'I3XXHNZFS5', 'momoeko@icloud.com', '838-674-0508', 'F', '6', 'd', 'FuYiJgqojl', '2005-01-24 09:13:25', '2007-05-27', '2014-07-02 18:57:38', 'id96Rd7LN6');

库2

sql 复制代码
CREATE DATABASE `db_datasource2` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_dept
-- ----------------------------
DROP TABLE IF EXISTS `sys_dept`;
CREATE TABLE `sys_dept`  (
  `dept_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '部门id',
  `parent_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '父部门id',
  `ancestors` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '祖级列表',
  `dept_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门名称',
  `sort` int NULL DEFAULT 0 COMMENT '显示顺序',
  `leader` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '负责人',
  `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系电话',
  `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '部门状态(0正常 1停用)',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
  `create_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`dept_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '部门' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

INSERT INTO `sys_dept` VALUES ('1', 'HUgJ0Q5LFk', 'Nj2g1qDopB', 'Lillian Scott', 949, 'kJd3YtR37A', '330-960-0289', 'lilsc@mail.com', 'H', 'a', 'nW7JbRL1lW', '2017-01-31 11:27:19', '2020-04-25', '2021-08-05 21:01:30', 'CtnWDnTMLb');
INSERT INTO `sys_dept` VALUES ('10', 'LiyW45NXM6', 'lRC9gwAR9x', 'Zou Xiuying', 27, 'mrAsaP3Hp9', '182-6382-7016', 'zou1997@gmail.com', '7', 'D', 'K4JIkvIrZG', '2009-10-30 19:08:55', '2017-03-30', '2021-10-17 03:48:53', 'wqkUJDbSMb');
INSERT INTO `sys_dept` VALUES ('2', 'mZs4KoUT3C', 'j4tO6YplDj', 'Yamaguchi Seiko', 119, 'EojAdin4MZ', '90-0704-3278', 'yseik@gmail.com', 'G', 'F', 'vLtNL0lx2y', '2002-03-31 09:55:24', '2012-05-21', '2021-08-15 15:10:50', 'MnXylte5qG');
INSERT INTO `sys_dept` VALUES ('3', 'jr9CY8BS5f', 'pgbuQ30O73', 'Monica Jones', 680, 'TatwoIxlvF', '(116) 601 4171', 'jonesmo@hotmail.com', 'I', '1', 'uZXxd7exsd', '2006-07-04 07:52:47', '2006-06-18', '2009-07-08 12:37:22', 'B9Ki28jjWs');
INSERT INTO `sys_dept` VALUES ('4', 'EkH6RjIUV0', 'RclPd0DxWx', 'Kwan Ling Ling', 295, 'A5R6LTZDuM', '614-058-4648', 'klingling@icloud.com', 'o', 'B', 'c3UcvVrDbO', '2017-12-17 18:14:00', '2004-09-29', '2003-01-28 21:03:24', 'Y8K4Dkm7UW');
INSERT INTO `sys_dept` VALUES ('5', 'iP9wtBe2fH', 'dtO7pM8Iow', 'Lei Shihan', 6, 'VEKw58FFri', '183-4374-4355', 'lesh@outlook.com', '7', 'X', '73vEosRJhr', '2012-10-29 00:41:19', '2005-12-31', '2023-12-25 18:00:29', 'fqIt2FJB2e');
INSERT INTO `sys_dept` VALUES ('6', 'efebc5Kc9m', 'BcdTSBuYaU', 'Fujiwara Misaki', 57, 'EL2Qcaus76', '5155 518752', 'mfujiwara@gmail.com', '1', 't', 'IBxSGb0kDa', '2013-10-13 11:46:15', '2010-12-17', '2005-11-15 03:48:31', 'k8CbC1FeH8');
INSERT INTO `sys_dept` VALUES ('7', '7nu5LmFK6Y', 'PXDFTdfXYj', 'Li Rui', 847, 'CiYQstpf7b', '(1223) 06 7489', 'ruili@outlook.com', '3', 'Z', 'cgADm8ZxpO', '2001-06-20 20:31:16', '2021-12-21', '2010-06-15 18:21:14', 'R1iUg5Qurx');
INSERT INTO `sys_dept` VALUES ('8', 'oVXgc9WjJD', 'Vh7XjZiMqp', 'Benjamin Salazar', 479, 'k3XWxcqbA3', '146-2118-0700', 'besalazar@mail.com', 'X', 'u', 'jE2AhPjII4', '2004-01-27 05:48:09', '2007-03-07', '2012-04-18 17:34:09', 'v5TDvPBmTB');
INSERT INTO `sys_dept` VALUES ('9', 'IS4SFMoET8', 'gLaql9lHJE', 'Ying Kar Yan', 472, 'UWrxMmxJ33', '90-6411-1494', 'ykaryan@hotmail.com', '2', '2', 'k6wiSiJpm9', '2012-09-19 17:27:00', '2009-07-10', '2007-01-28 16:27:44', 'EPdTrljVr1');

最后效果如下

数据源配置

sql 复制代码
spring:
    dynamic:
      datasource:
        datasource1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://ip:3306/db_datasource1?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useInformationSchema=true
          username: root
          password: root
          initial-size: 10
          max-active: 100
          min-idle: 10
          max-wait: 60000
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          aop-patterns: "com.dfec.module.*.service.*"
    
        datasource2:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://ip:3306/db_datasource2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useInformationSchema=true
          username: root
          password: root
          initial-size: 10
          max-active: 100
          min-idle: 10
          max-wait: 60000
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          aop-patterns: "com.dfec.module.*.service.*"

测试

自动切换

这里通过注解的形式实现自动切换

java 复制代码
@RestController
@RequestMapping("test")
@RequiredArgsConstructor
public class TestController {

    private final SysDeptService sysDepetService;

    private final SysUserService sysUserService;


        @DataSource(value = "datasource1")
        @GetMapping("/test1")
        public AjaxResult test(){
            return AjaxResult.ok().put(sysUserService.list());
        }
        
        
        @DataSource(value = "datasource2")
        @GetMapping("/test2")
        public AjaxResult test2(){
            return AjaxResult.ok( ).put(sysDepetService.list());
        }



}

我们使用apifox进行调用测试

测试接口1

数据与数据库一致,即测试通过

测试接口2

数据与数据库一致,即测试通过

手动切换

对代码进行改造,我们借助JdbcDataSourceRouter类进行手动切换

java 复制代码
@GetMapping("/test1")
public AjaxResult test(){
    JdbcDataSourceRouter.setDataSourceKey("datasource1");
    List<SysUserEntity> list = sysUserService.list();
    JdbcDataSourceRouter.remove();
    return AjaxResult.ok().put(list);
}


@GetMapping("/test2")
public AjaxResult test2(){
    JdbcDataSourceRouter.setDataSourceKey("datasource2");
    List<SysDeptEntity> list = sysDepetService.list();
    JdbcDataSourceRouter.remove();
    return AjaxResult.ok( ).put(list);
}

测试接口1

测试通过

测试接口2

实现原理

AbstractRoutingDataSource类由springboot提供,它根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

简单理解就是SQL在实际执行之前才设置数据源,决定由哪个数据源去执行

实现流程

定义AbstractRoutingDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。

  1. determineCurrentLookupKey()方法每次获取当前数据源key的时候都是从当前threadLoad本地线程中去获取
  2. 然后determineTargetDataSource()方法通过从本地线程中获取到的当前数据源id去切换对应的数据源
java 复制代码
/**
 * 切换数据源
 *
 * @return
 */
@Override
protected DataSource determineTargetDataSource() {
    //获取到当前数据源id
    Object dataSourceKey = this.determineCurrentLookupKey();
    // 默认系统数据源
    if (dataSourceKey == null||dataSourceKey.equals(BaseConstant.DEFAULT_DATA_SOURCE_ID)) {
        return super.getResolvedDefaultDataSource();
    }
    String dataSourceId = getDataSourceId(dataSourceKey.toString());
    //从 DataSourceManager.DATA_SOURCE_POOL_JDBC中获取到当前
    DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(dataSourceId);

    if (dataSource == null) {
        throw new ServiceException("无效的数据源:" + dataSourceId, 44003);
    }
    return dataSource;
}

那么下面我们只要追溯一下三个问题即可

第一个、DataSourceManager.DATA_SOURCE_POOL_JDBC中的数据是怎么来的,因为上面是从这个里面去获取配置的数据源的

第二个、本地线程中的数据源id是怎么塞进去的

这里我们是不是只要手动塞进去数据源id,那么是不是就可以走多数据源配置了?答案是肯定,我们接着往下看

第三个、系统数据源是怎么定义的

只要解决 这两个问题,那么多数据源流程我们就搞清楚了

以下我将先介绍几个核心类

核心类:AbstractRoutingDataSource

那我们就先揭开这个类的神秘面纱

java 复制代码
/**
 * 抽象类,用于根据当前的上下文信息动态地选择要使用的DataSource。
 * 需要由子类实现determineCurrentLookupKey()方法,以定义如何根据当前上下文确定要使用的DataSource。
 */
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    @Nullable
    private Map<Object, Object> targetDataSources; // 目标DataSource的映射,键为查找键,值为DataSource实例或名称字符串。

    @Nullable
    private Object defaultTargetDataSource; // 默认的目标DataSource,当没有匹配的DataSource时使用。

    private boolean lenientFallback = true; // 是否允许宽容的回退到默认的DataSource。

    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); // 用于解析DataSource名称的查找器。

    @Nullable
    private Map<Object, DataSource> resolvedDataSources; // 解析后的DataSource映射。

    @Nullable
    private DataSource resolvedDefaultDataSource; // 解析后的默认DataSource.


    /**
     * 设置目标DataSource的映射,键为查找键,值可以是对应的{@link javax.sql.DataSource}实例或DataSource名称字符串(通过{@link #setDataSourceLookup DataSourceLookup}解析)。
     * 查找键可以是任意类型,具体处理由{@link #resolveSpecifiedLookupKey(Object)}和{@link #determineCurrentLookupKey()}完成。
     */
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    /**
     * 设置默认的目标DataSource,如果没有匹配的键,则使用此DataSource。
     * 值可以是对应的{@link javax.sql.DataSource}实例或DataSource名称字符串(通过{@link #setDataSourceLookup DataSourceLookup}解析)。
     */
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    /**
     * 设置当找不到匹配的DataSource时,是否宽容地回退到默认的DataSource。
     * 默认为"true",允许查找键没有对应条目的情况,此时会回退到默认的DataSource。
     * 如果将此标志设置为"false",则只有在查找键为{@code null}时才会回退到默认的DataSource,没有对应条目的查找键会导致IllegalStateException异常。
     * @see #setTargetDataSources
     * @see #setDefaultTargetDataSource
     * @see #determineCurrentLookupKey()
     */
    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    /**
     * 设置用于解析目标DataSource映射中DataSource名称字符串的DataSourceLookup实现。
     * 默认为{@link JndiDataSourceLookup},允许直接指定应用服务器DataSources的JNDI名称。
     */
    public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    }


    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

    /**
     * 初始化时调用,用于解析指定的查找键对象,转化为实际匹配的查找键。
     * 默认实现直接返回给定的查找键。
     * @param lookupKey 用户指定的查找键对象
     * @return 实际用于匹配的查找键
     */
    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    /**
     * 初始化时调用,用于解析指定的DataSource对象或名称,转化为DataSource实例。
     * 默认实现支持DataSource实例和DataSource名称字符串(通过{@link DataSourceLookup}解析)。
     * @param dataSource 目标DataSource的值对象,可以是实例或名称
     * @return 解析后的DataSource实例
     * @throws IllegalArgumentException 如果给定的值类型不支持
     */
    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource) dataSource;
        }
        else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String) dataSource);
        }
        else {
            throw new IllegalArgumentException(
                    "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }

    /**
     * 返回此路由器管理的已解析目标DataSources。
     * @return 一个不可修改的映射,包含解析后的查找键和DataSources
     * @throws IllegalStateException 如果目标DataSources尚未解析
     * @since 5.2.9
     * @see #setTargetDataSources
     */
    public Map<Object, DataSource> getResolvedDataSources() {
        Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet");
        return Collections.unmodifiableMap(this.resolvedDataSources);
    }

    /**
     * 返回已解析的默认目标DataSource,如果未指定或尚未解析,则返回null。
     * @return 默认的DataSource,或者null(如果未指定或尚未解析)
     * @since 5.2.9
     * @see #setDefaultTargetDataSource
     */
    @Nullable
    public DataSource getResolvedDefaultDataSource() {
        return this.resolvedDefaultDataSource;
    }


    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> iface) throws SQLException {
        if (iface.isInstance(this)) {
            return (T) this;
        }
        return determineTargetDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
    }

    /**
     * 确定当前要使用的DataSource。此方法会根据{@link #determineCurrentLookupKey() 当前查找键}在{@link #setTargetDataSources 目标DataSources映射}中查找,
     * 如果没有找到匹配的DataSource且{@link #setLenientFallback 宽容回退}为true或查找键为null,则会回退到指定的{@link #setDefaultTargetDataSource 默认目标DataSource}。
     * @return 当前应使用的DataSource
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

    /**
     * 确定当前的查找键。通常,此方法会检查绑定到线程的事务上下文。
     * 允许返回任意类型的键,返回的键需要与通过{@link #resolveSpecifiedLookupKey}方法解析的存储查找键类型匹配。
     */
    @Nullable
    protected abstract Object determineCurrentLookupKey();

}

此类为抽象类,要求我们去实现此类,同时去实现里面的determineCurrentLookupKey(),来决定我们到底要使用哪个数据源

实现类JdbcDataSourceRouter

当前类主要是AbstractRoutingDataSource的实现类,并重写了determineCurrentLookupKey()、determineTargetDataSource()方法

java 复制代码
package com.dfec.framework.datasourse.core.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.dfec.common.exception.ServiceException;
import com.dfec.framework.datasourse.core.constant.BaseConstant;
import com.dfec.framework.datasourse.core.driver.DataSourceManager;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * JDBC数据源 动态路由
 *
 * 
 */
public class JdbcDataSourceRouter extends AbstractRoutingDataSource {

    /**
     * 当前线程数据源KEY
     */
    private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();


    /**
     * 获取数据源key
     *
     * @return
     */
    public static String getDataSourceKey() {
        return DATA_SOURCE_KEY.get();
    }

    /**
     * 设置数据源key
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        DATA_SOURCE_KEY.set(key);
    }

    /**
     * 移除数据源
     */
    public static void remove() {
        DATA_SOURCE_KEY.remove();
    }

    /**
     * 判断数据源是否存在
     */
    public static boolean exist(String dataSourceId) {
        DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceId));
        if (dataSource != null) {
            return true;
        }
        return false;
    }

    /**
     * 获取数据源ID
     *
     * @param dataSourceId
     * @return
     */
    private static String getDataSourceId(String dataSourceId) {
        return dataSourceId.split(":")[0];
    }

    /**
     * 销毁
     *
     * @param dataSourceId
     * @return
     */
    public static void destroy(String dataSourceId) {
        DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceId));
        if (dataSource instanceof DruidDataSource) {
            DruidDataSource druidDataSource = (DruidDataSource) dataSource;
            druidDataSource.close();
        } else if (dataSource instanceof HikariDataSource) {
            HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
            hikariDataSource.close();
        }
        DataSourceManager.DATA_SOURCE_POOL_JDBC.remove(dataSourceId);
    }

    /**
     * 获取数据源
     *
     * @param dataSourceId
     * @return
     */
    public static DataSource getDataSource(String dataSourceId) {
        DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceId));
        if (dataSource == null) {
            throw new ServiceException("无效的数据源", 44003);

        }
        return dataSource;
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public static DataSource getDataSource() {
        String dataSourceKey = getDataSourceKey();
        DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceKey));
        if (dataSource == null) {
            throw new ServiceException("无效的数据源", 44003);
        }

        return dataSource;
    }

    /**
     * 添加数据源
     *
     * @param dataSourceId
     **/
    public static void setDataSource(String dataSourceId, DataSource dataSource) {
        DataSourceManager.DATA_SOURCE_POOL_JDBC.put(dataSourceId, dataSource);
    }

    /**
     * 切换数据源
     *
     * @return
     */
    @Override
    protected DataSource determineTargetDataSource() {
        Object dataSourceKey = this.determineCurrentLookupKey();
        // 默认系统数据源
        if (dataSourceKey == null||dataSourceKey.equals(BaseConstant.DEFAULT_DATA_SOURCE_ID)) {
            return super.getResolvedDefaultDataSource();
        }
        String dataSourceId = getDataSourceId(dataSourceKey.toString());
        DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(dataSourceId);

        if (dataSource == null) {
            throw new ServiceException("无效的数据源:" + dataSourceId, 44003);
        }
        return dataSource;
    }

    /**
     * 获取连接
     *
     * @return
     * @throws SQLException
     */
    @Override
    public Connection getConnection() throws SQLException {
        Connection connection = this.determineTargetDataSource().getConnection();
        return connection;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSourceKey();
    }
}

数据源管理类DataSourceManager

java 复制代码
package com.dfec.framework.datasourse.core.driver;

import com.dfec.common.exception.ServiceException;
import com.dfec.framework.datasource.spi.DataSourceDriver;
import com.dfec.framework.datasource.spi.MetaDataMapper;
import com.dfec.framework.datasource.spi.StatementMapper;
import com.dfec.framework.datasource.spi.bean.BaseDataSource;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 数据源驱动管理
 *
 */
public class DataSourceManager {

    /**
     * 数据源插件类型
     */
    private final Map<String, DataSourceDriver> PLUGIN_TYPE = new ConcurrentHashMap<>();

    /**
     * 数据源插驱动接池
     */
    private final Map<String, DataSourceDriver> DATA_SOURCE_POOL_PLUGIN = new ConcurrentHashMap<>();

    /**
     * JDBC数据源连接池
     */
    public static final Map<String, DataSource> DATA_SOURCE_POOL_JDBC = new ConcurrentHashMap<>();

    /**
     * 支持的关系型数据源类型
     */
    private static final List<String> JDBC_DATA_SOURCE_TYPE = Arrays.asList("mysql", "mariadb", "oracle", "sqlserver", "postgresql", "db2", "sqlite", "tidb", "opengauss", "oceanbase", "polardb", "tdsql", "dm", "gbase", "hive2","xugu","clickhouse");
    /**
     * 默认数据源驱动实现
     */
    private DataSourceDriver<?> defaultDriver;

    public DataSourceManager(DataSourceDriver<?> dataSourceDriver) {
        // 默认JDBC驱动
        this.defaultDriver = dataSourceDriver;
        // 初始化加载驱动插件
        init();
    }

    /**
     * 初始化加载插件驱动
     */
    public void init() {
        ServiceLoader<DataSourceDriver> loader = ServiceLoader.load(DataSourceDriver.class);
        for (DataSourceDriver driver : loader) {
            String driverName = driver.getName();
            if (driverName == null || "".equals(driverName)) {
                throw new ServiceException( "插件名称不能为空",51003);
            }
            if (PLUGIN_TYPE.containsKey(driverName)) {
                throw new ServiceException("该插件名称" + driverName + "已存在",51004);
            }
            PLUGIN_TYPE.putIfAbsent(driverName.toLowerCase(), driver);
        }
    }

    /**
     * 插件创建数据源
     *
     * @param dataSource
     */
    public void createDataSource(BaseDataSource dataSource) {
        String datasourceType = dataSource.getDatasourceType();
        DataSourceDriver<?> dataSourceDriver = PLUGIN_TYPE.get(datasourceType);
        if (dataSourceDriver != null) {
            // 初始化
            dataSourceDriver.init(dataSource);
            DATA_SOURCE_POOL_PLUGIN.putIfAbsent(dataSource.getDatasourceId(), dataSourceDriver);
        } else {
            dataSourceDriver = this.defaultDriver;
            dataSourceDriver.init(dataSource);
        }
    }

    /**
     * 测试数据源
     *
     * @param dataSource
     * @return
     */
    public String test(BaseDataSource dataSource) {
        String datasourceType = dataSource.getDatasourceType();
        DataSourceDriver<?> dataSourceDriver = PLUGIN_TYPE.get(datasourceType);
        if (dataSourceDriver != null) {
            return dataSourceDriver.test(dataSource);
        } else if (JDBC_DATA_SOURCE_TYPE.contains(datasourceType)) {
            return defaultDriver.test(dataSource);
        } else {
            throw new ServiceException("暂不支持" + datasourceType + "数据源类型!",51001);
        }
    }


    /**
     * 获取数据源驱动
     *
     * @param datasourceId
     * @return
     */
    public DataSourceDriver<?> getDataSource(String datasourceId) {
        DataSourceDriver<?> dataSourceDriver = null;
        DataSource dataSource = DATA_SOURCE_POOL_JDBC.get(datasourceId);
        if (dataSource != null) {
            dataSourceDriver = this.defaultDriver;
        } else {
            dataSourceDriver = DATA_SOURCE_POOL_PLUGIN.get(datasourceId);
        }
        if (dataSourceDriver == null) {
            throw new ServiceException("数据源不存在!",51001);
        }
        return dataSourceDriver;
    }

    /**
     * 元数据对象
     * @param datasourceId
     * @return
     */
    public MetaDataMapper<?> getMetaData(String datasourceId){
        DataSourceDriver<?> dataSource = this.getDataSource(datasourceId);
        return dataSource.getMetaData();
    }

    /**
     * 数据处理对象
     * @param datasourceId
     * @return
     */
    public StatementMapper<?> getStatementMapper(String datasourceId){
        DataSourceDriver<?> dataSource = this.getDataSource(datasourceId);
        return dataSource.getStatement();
    }
    
    /**
     * 删除数据源驱动
     *
     * @param datasourceId
     */
    public void remove(String datasourceId) {
        DataSourceDriver<?> dataSourceDriver = DATA_SOURCE_POOL_PLUGIN.get(datasourceId);
        if (dataSourceDriver != null) {
            dataSourceDriver.destroy(datasourceId);
            DATA_SOURCE_POOL_PLUGIN.remove(datasourceId);
        }
        DataSource dataSource = DATA_SOURCE_POOL_JDBC.get(datasourceId);
        if (dataSource != null && dataSource instanceof HikariDataSource) {
            HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
            hikariDataSource.close();
        }
        DATA_SOURCE_POOL_JDBC.remove(datasourceId);
    }
}

配置类DynamicDataSourceConfig

java 复制代码
package com.dfec.framework.datasourse.core.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.dfec.framework.datasource.spi.DataSourceDriver;
import com.dfec.framework.datasourse.core.constant.BaseConstant;
import com.dfec.framework.datasourse.core.driver.DataSourceManager;
import com.dfec.framework.datasourse.core.driver.jdbc.JdbcDataSourceDriver;
import com.dfec.framework.datasourse.core.factory.DataSourceFactory;
import com.dfec.framework.datasourse.core.mapper.BaseDataHandleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;


/**
 * 数据源路由配置
 *
 */
@Configuration
public class DynamicDataSourceConfig {

    @Resource
    private DynamicDataSourceProperties properties;
    /**
     * 数据源配置
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 默认系统数据源
     *
     * @return
     */
    @Bean
    public JdbcDataSourceRouter defaultDataSource() {
        DruidDataSource dataSource = DataSourceFactory.buildDruidDataSource(dataSourceProperties());
        JdbcDataSourceRouter dynamic = new JdbcDataSourceRouter();
        dynamic.setTargetDataSources(getDynamicDataSource());
        // 设置默认数据源
        dynamic.setDefaultTargetDataSource(dataSource);
        JdbcDataSourceRouter.setDataSourceKey(BaseConstant.DEFAULT_DATA_SOURCE_ID);
        JdbcDataSourceRouter.setDataSource(BaseConstant.DEFAULT_DATA_SOURCE_ID, dataSource);
        return dynamic;
    }

    private Map<Object, Object> getDynamicDataSource() {
        Map<String, DataSourceProperties> dataSourcePropertiesMap = this.properties.getDatasource();
        Map<Object, Object> targetDataSources = new HashMap(dataSourcePropertiesMap.size());
        dataSourcePropertiesMap.forEach((k, v) -> {
            DruidDataSource druidDataSource = DataSourceFactory.buildDruidDataSource(v);
            targetDataSources.put(k, druidDataSource);
            JdbcDataSourceRouter.setDataSource(k, druidDataSource);
        });
        return targetDataSources;
    }

    /**
     * JDBC 数据源驱动
     *
     * @param baseDataHandleMapper
     * @return
     */
    @Bean
    public DataSourceDriver dataSourceDriver(BaseDataHandleMapper baseDataHandleMapper) {
        return new JdbcDataSourceDriver(baseDataHandleMapper);
    }

    /**
     * 数据源插件驱动
     *
     * @param jdbcDataSourceDriver
     * @return
     */
    @Bean
    public DataSourceManager dataSourceDriverManager(DataSourceDriver jdbcDataSourceDriver) {
        DataSourceManager driverManager = new DataSourceManager(jdbcDataSourceDriver);
        return driverManager;
    }
}

注意:这里回答我们上面的问题

再回顾一下上面三个问题

第一个、DataSourceManager.DATA_SOURCE_POOL_JDBC中的数据是怎么来的

回答:在配置文件中通过 JdbcDataSourceRouter.setDataSource(k, druidDataSource);进行设置的

java 复制代码
public static void setDataSource(String dataSourceId, DataSource dataSource) {
    DataSourceManager.DATA_SOURCE_POOL_JDBC.put(dataSourceId, dataSource);
}

而这对应的k和druidDataSource信息是从yml配置文件中读取进来的

第二个、本地线程中的数据源id是怎么塞进去的

回答:这里我们在测试代码的时候其实就已经看到了,通过手动去切换的,就这段代码,记得吧

java 复制代码
@GetMapping("/test1")
public AjaxResult test(){
    JdbcDataSourceRouter.setDataSourceKey("datasource1");
    List<SysUserEntity> list = sysUserService.list();
    JdbcDataSourceRouter.remove();
    return AjaxResult.ok().put(list);
}

第三个、系统数据源是怎么定义的

回答:构建默认的数据源 DruidDataSource dataSource = DataSourceFactory.buildDruidDataSource(dataSourceProperties());

java 复制代码
public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
    DruidDataSource druidDataSource = new DruidDataSource();
    druidDataSource.setDriverClassName(properties.getDriverClassName());
    druidDataSource.setUrl(properties.getUrl());
    druidDataSource.setUsername(properties.getUsername());
    druidDataSource.setPassword(properties.getPassword());
    druidDataSource.setInitialSize(properties.getInitialSize());
    druidDataSource.setMaxActive(properties.getMaxActive());
    druidDataSource.setMinIdle(properties.getMinIdle());
    druidDataSource.setMaxWait(properties.getMaxWait());
    druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
    druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
    druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
    druidDataSource.setValidationQuery(properties.getValidationQuery());
    druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
    druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
    druidDataSource.setTestOnReturn(properties.isTestOnReturn());
    druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
    druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
    druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());

    try {
        druidDataSource.setFilters(properties.getFilters());
        druidDataSource.init();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return druidDataSource;
}

查看这个方法defaultDataSource()

java 复制代码
@Bean
public JdbcDataSourceRouter defaultDataSource() {
    DruidDataSource dataSource = DataSourceFactory.buildDruidDataSource(dataSourceProperties());
    JdbcDataSourceRouter dynamic = new JdbcDataSourceRouter();
    //设置目标数据源 ,即动态数据源
    dynamic.setTargetDataSources(getDynamicDataSource());
    // 设置默认数据源
    dynamic.setDefaultTargetDataSource(dataSource);
    JdbcDataSourceRouter.setDataSourceKey(BaseConstant.DEFAULT_DATA_SOURCE_ID);
    JdbcDataSourceRouter.setDataSource(BaseConstant.DEFAULT_DATA_SOURCE_ID, dataSource);
    return dynamic;
}

private Map<Object, Object> getDynamicDataSource() {
    //入口1,注意这里的入口是我们通过yml进行配置多数据源的
    Map<String, DataSourceProperties> dataSourcePropertiesMap = this.properties.getDatasource();
    Map<Object, Object> targetDataSources = new HashMap(dataSourcePropertiesMap.size());
    dataSourcePropertiesMap.forEach((k, v) -> {
        DruidDataSource druidDataSource = DataSourceFactory.buildDruidDataSource(v);
        targetDataSources.put(k, druidDataSource);
        JdbcDataSourceRouter.setDataSource(k, druidDataSource);
    });
    return targetDataSources;
}

下面我再附上通过注解动态切换数据源

通过注解动态切换数据源

java 复制代码
package com.dfec.framework.datasourse.core.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 多数据源注解
 *
 * @date 2023-12-01 16:12:51
 */
@Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    String value() default "系统数据源";
}
java 复制代码
package com.dfec.framework.datasourse.core.aspect;

import com.dfec.common.utils.str.StringUtils;
import com.dfec.framework.datasourse.core.annotation.DataSource;
import com.dfec.framework.datasourse.core.config.JdbcDataSourceRouter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Objects;

/**
 * 多数据源切面
 */
@Aspect
@Component
@Order(-2147483648)
public class DataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(com.dfec.framework.datasourse.core.annotation.DataSource) || @within(com.dfec.framework.datasourse.core.annotation.DataSource)")
    public void dataSourcePointCut() {}

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);
        if (StringUtils.isNotNull(dataSource)) {
            JdbcDataSourceRouter.setDataSourceKey(dataSource.value());
        }
        try {
            return point.proceed();
        } finally {
            JdbcDataSourceRouter.remove();
            this.logger.debug("clean datasource");
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

总结

  1. 定义AbstractRoutingDataSource类继承抽象类AbstractRoutingDataSource,并实现了以下两个方法

determineCurrentLookupKey()方法每次获取当前数据源key的时候都是从当前threadLoad本地线程中去获取

determineTargetDataSource()方法通过从本地线程中获取到数据源id去切换对应的数据源

  1. 通过手动去设置数据源id,可以实现数据源的切换,我们这里只需要去开发一个注解简化切换
  2. 数据源信息主要存储在DataSourceManager.DATA_SOURCE_POOL_JDBC集合中,它是在项目启动的时候通过配置DynamicDataSourceConfig#defaultDataSource()的方法进行设置了动态数据源和默认的数据源信息

扩展

以上配置方式给我提供了入口,那么我们可以尝试将数据源结合表进行管理,这样可以实现动态的管理数据源了,篇幅有限,后面单独写吧

相关推荐
徐*红几秒前
java 线程池
java·开发语言
尚学教辅学习资料几秒前
基于SSM的养老院管理系统+LW示例参考
java·开发语言·java毕设·养老院
2401_85763639几秒前
计算机课程管理平台:Spring Boot与工程认证的结合
java·spring boot·后端
1 9 J3 分钟前
Java 上机实践4(类与对象)
java·开发语言·算法
Code apprenticeship3 分钟前
Java面试题(2)
java·开发语言
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
霖雨3 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404193 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java