小小多数据源,拿下拿下

前言

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

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

数据源(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()的方法进行设置了动态数据源和默认的数据源信息

扩展

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

相关推荐
盖世英雄酱581361 分钟前
java 深度调试【第一章:堆栈分析】
java·后端
lastHertz18 分钟前
Golang 项目中使用 Swagger
开发语言·后端·golang
渣哥19 分钟前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜24 分钟前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始28 分钟前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴41 分钟前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年1 小时前
用Go实现类似WinGet风格彩色进度条
后端
蓝色汪洋1 小时前
string字符集
java·开发语言
卿言卿语1 小时前
CC1-二叉树的最小深度
java·数据结构·算法·leetcode·职场和发展
淳朴小学生1 小时前
静态代理和动态代理
后端